]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/API2/Subscription.pm
website: update external links to www.proxmox.com
[pve-manager.git] / PVE / API2 / Subscription.pm
index 52e66d3b6d7af7e7e4351b8627fbf6ff635ed5eb..c9db8d986338dcada3f91161e813071347bea246 100644 (file)
@@ -6,13 +6,16 @@ use Digest::MD5 qw(md5_hex md5_base64);
 use MIME::Base64;
 use HTTP::Request;
 use LWP::UserAgent;
-use JSON; 
+use JSON;
+
+use Proxmox::RS::Subscription;
 
 use PVE::Tools;
 use PVE::ProcFSTools;
 use PVE::Exception qw(raise_param_exc);
 use PVE::INotify;
 use PVE::Cluster qw (cfs_read_file cfs_write_file);
+use PVE::DataCenterConfig;
 use PVE::AccessControl;
 use PVE::Storage;
 use PVE::JSONSchema qw(get_standard_option);
@@ -24,29 +27,8 @@ use PVE::RESTHandler;
 
 use base qw(PVE::RESTHandler);
 
-PVE::INotify::register_file('subscription', "/etc/pve-subscription",
-                           \&read_etc_pve_subscription,
-                           \&write_etc_pve_subscription);
-
-# How long the local key is valid for in between remote checks
-my $localkeydays = 15; 
-# How many days to allow after local key expiry before blocking 
-# access if connection cannot be made
-my $allowcheckfaildays = 5;
-
-my $shared_key_data = "kjfdlskfhiuewhfk947368";
-my $hwaddress;
-
-sub get_hwaddress {
-    
-    return $hwaddress if defined ($hwaddress);
-
-    my $fn = '/etc/ssh/ssh_host_rsa_key.pub';
-    my $sshkey = PVE::Tools::file_get_contents($fn);
-    $hwaddress = uc(md5_hex($sshkey));
-
-    return $hwaddress;
-}
+my $subscription_pattern = 'pve([1248])([cbsp])-[0-9a-f]{10}';
+my $filename = "/etc/subscription";
 
 sub get_sockets {
     my $info = PVE::ProcFSTools::read_cpuinfo();
@@ -54,197 +36,70 @@ sub get_sockets {
 }
 
 sub parse_key {
-    my ($key) = @_;
+    my ($key, $noerr) = @_;
 
-    if ($key =~ m/^pve([124])([cbsp])-[0-9a-f]{10}$/) {
+    if ($key =~ m/^${subscription_pattern}$/) {
        return wantarray ? ($1, $2) : $1; # number of sockets, level
     }
-    return undef;
-}
+    return undef if $noerr;
 
-sub check_fields {
-    my ($info, $server_id, $req_sockets) = @_;
+    die "Wrong subscription key format\n";
+}
 
-    foreach my $f (qw(status checktime key)) {
-       if (!$info->{$f}) {
-           die "Missing field '$f'\n";
-       }
-    }
+sub check_key {
+    my ($key, $req_sockets) = @_;
 
-    my $sockets = parse_key($info->{key});
-    if (!$sockets) {
-       die "Wrong subscription key format\n";
-    }
+    my ($sockets, $level) = parse_key($key);
     if ($sockets < $req_sockets) {
        die "wrong number of sockets ($sockets < $req_sockets)\n";
     }
-
-    if ($info->{checktime} > time()) {
-       die "Last check time in future.\n";
-    }
-
-    return undef if $info->{status} ne 'Active';
-
-    foreach my $f (qw(validdomain productname regdate nextduedate)) {
-       if (!$info->{$f}) {
-           die "Missing field '$f'\n";
-       }
-    }
-
-    my $found;
-    foreach my $dom (split(/,/, $info->{validdomain})) {
-       if ($dom eq $server_id) {
-           $found = 1;
-           last;
-       }
-    }
-    die "Server ID does not match\n" if !$found;
-
-    return undef;
+    return ($sockets, $level);
 }
 
-sub read_etc_pve_subscription {
-    my ($filename, $fh) = @_;
-
-    my $info = { status => 'Invalid' };
-
-    my $key = <$fh>; # first line is the key
-    chomp $key;
-    my ($sockets, $level) = parse_key($key);
-    die "Wrong subscription key format\n" if !$sockets;
-
-    my $csum = <$fh>; # second line is a checksum
-
-    $info->{key} = $key;
-
-    my $data = '';
-    while (defined(my $line = <$fh>)) {
-       $data .= $line;
-    }
-
-    if ($csum && $data) {
-
-       chomp $csum;
-    
-       my $localinfo = {};
-
-       eval {
-           my $json_text = decode_base64($data);
-           $localinfo = decode_json($json_text);
-           my $newcsum = md5_base64($localinfo->{checktime} . $data . $shared_key_data);
-           die "checksum failure\n" if $csum ne $newcsum;
-
-           my $req_sockets = get_sockets();
-           my $server_id = get_hwaddress();
-
-           check_fields($localinfo, $server_id, $req_sockets);
+sub read_etc_subscription {
+    my $req_sockets = get_sockets();
+    my $server_id = PVE::API2Tools::get_hwaddress();
 
-           my $age = time() -  $localinfo->{checktime};
+    my $info = Proxmox::RS::Subscription::read_subscription($filename);
 
-           my $maxage = ($localkeydays + $allowcheckfaildays)*60*60*24;
-           if ($localinfo->{status} eq 'Active' && $age > $maxage) {
-               $localinfo->{status} = 'Invalid';
-               $localinfo->{message} = "subscription info too old";
-           }
-       };
-       if (my $err = $@) {
-           warn $err;
-       } else {
-           $info = $localinfo;
-       }
-    }
+    return $info if !$info || $info->{status} ne 'active';
 
-    if ($info->{status} eq 'Active') {
+    my ($sockets, $level);
+    eval { ($sockets, $level) = check_key($info->{key}, $req_sockets); };
+    if (my $err = $@) {
+       chomp $err;
+       $info->{status} = 'invalid';
+       $info->{message} = $err;
+    } else {
        $info->{level} = $level;
     }
 
     return $info;
 }
 
-sub write_etc_pve_subscription {
-    my ($filename, $fh, $info) = @_;
-
-    if ($info->{status} eq 'New') {
-       PVE::Tools::safe_print($filename, $fh, "$info->{key}\n");
-       return;
-    }
-
-    my $json = encode_json($info);
-    my $data = encode_base64($json);
-    my $csum = md5_base64($info->{checktime} . $data . $shared_key_data);
-    
-    my $raw = "$info->{key}\n$csum\n$data";
-
-    PVE::Tools::safe_print($filename, $fh, $raw);
-}
+sub write_etc_subscription {
+    my ($info) = @_;
 
-sub check_subscription {
-    my ($key) = @_;
+    my $server_id = PVE::API2Tools::get_hwaddress();
+    mkdir "/etc/apt/auth.conf.d";
+    Proxmox::RS::Subscription::write_subscription($filename, "/etc/apt/auth.conf.d/pve.conf", "enterprise.proxmox.com/debian/pve", $info);
 
-    my $whmcsurl = "http://shop2.maurer-it.com";
-
-    my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
-    my $server_id = get_hwaddress();
-
-    my $req_sockets = get_sockets();
-
-    my $check_token = time() . md5_hex(rand(8999999999) + 1000000000) . $key;
-
-    my $params = {
-       licensekey => $key,
-       domain => $server_id,
-       ip => 'localhost',
-       check_token => $check_token,
-    };
-
-    my $req = HTTP::Request->new('POST' => $uri);
-    $req->header('Content-Type' => 'application/x-www-form-urlencoded'); 
-    # We use a temporary URI object to format
-    # the application/x-www-form-urlencoded content.
-    my $url = URI->new('http:');
-    $url->query_form(%$params);
-    my $content = $url->query;
-    $req->header('Content-Length' => length($content));
-    $req->content($content);
-
-    my $ua = LWP::UserAgent->new(protocols_allowed => [ 'http', 'https' ], timeout => 30);
-    my $response = $ua->request($req);
-    my $code = $response->code;
-
-    if ($code != 200) {
-       my $msg = $response->message || 'unknown';
-       die "Invalid response from server: $code $msg\n";
+    # NOTE: preparation for easier upgrade to Proxmox VE 8, which introduced the ceph enterprise repo
+    my $ceph_auth = '';
+    for my $ceph_release ('quincy', 'reef') {
+       $ceph_auth .= "machine enterprise.proxmox.com/debian/ceph-${ceph_release}"
+           ." login $info->{key} password $info->{serverid}\n"
     }
-
-    my $raw = $response->decoded_content;
-
-    my $subinfo = {};
-    while ($raw =~ m/<(.*?)>([^<]+)<\/\1>/g) {
-       my ($k, $v) = ($1, $2);
-       $subinfo->{$k} = $v;
-    }
-    $subinfo->{checktime} = time();
-    $subinfo->{key} = $key;
-
-    my $emd5sum = md5_hex($shared_key_data . $check_token);
-    if ($subinfo->{status} && $subinfo->{status} eq 'Active') {
-       if (!$subinfo->{md5hash} || ($subinfo->{md5hash} ne $emd5sum)) {
-           die "MD5 Checksum Verification Failed\n";
-       }
-    }
-    
-    check_fields($subinfo, $server_id, $req_sockets);
-    return $subinfo;
+    PVE::Tools::file_set_contents("/etc/apt/auth.conf.d/ceph.conf", $ceph_auth);
 }
 
 __PACKAGE__->register_method ({
-    name => 'get', 
-    path => '', 
+    name => 'get',
+    path => '',
     method => 'GET',
     description => "Read subscription info.",
     proxyto => 'node',
+    permissions => { user => 'all' },
     parameters => {
        additionalProperties => 0,
        properties => {
@@ -255,20 +110,48 @@ __PACKAGE__->register_method ({
     code => sub {
        my ($param) = @_;
 
-       my $info = PVE::INotify::read_file('subscription');
+       my $node = $param->{node};
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+       my $has_permission = $rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'], 1);
+
+       my $server_id = PVE::API2Tools::get_hwaddress();
+       my $url = "https://www.proxmox.com/en/proxmox-virtual-environment/pricing";
+
+       my $info = read_etc_subscription();
        if (!$info) {
-           return {
-               status => "NotFound",
+           my $no_subscription_info = {
+               status => "notfound",
                message => "There is no subscription key",
+               url => $url,
+           };
+           $no_subscription_info->{serverid} = $server_id if $has_permission;
+           return $no_subscription_info;
+       }
+
+       if (!$has_permission) {
+           return {
+               status => $info->{status},
+               message => $info->{message},
+               url => $url,
            }
        }
+
+       $info->{serverid} = $server_id;
+       $info->{sockets} = get_sockets();
+       $info->{url} = $url;
+
        return $info
     }});
 
 __PACKAGE__->register_method ({
-    name => 'update', 
-    path => '', 
+    name => 'update',
+    path => '',
     method => 'POST',
+    permissions => {
+       check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+    },
     description => "Update subscription info.",
     proxyto => 'node',
     protected => 1,
@@ -288,37 +171,53 @@ __PACKAGE__->register_method ({
     code => sub {
        my ($param) = @_;
 
-       my $info = PVE::INotify::read_file('subscription');
+       my $info = read_etc_subscription();
        return undef if !$info;
 
-       if (!$param->{force} && $info->{status} eq 'Active') {
-           my $age = time() -  $info->{checktime};
-           return undef if $age < $localkeydays*60*60*24;
-       }
-       
+       my $server_id = PVE::API2Tools::get_hwaddress();
        my $key = $info->{key};
 
-       $info = check_subscription($key);
+       die "Updating offline key not possible - please remove and re-add subscription key to switch to online key.\n"
+           if $info->{signature};
+
+       # key has been recently checked
+       return undef
+           if !$param->{force}
+               && $info->{status} eq 'active'
+               && Proxmox::RS::Subscription::check_age($info, 1)->{status} eq 'active';
 
-       PVE::INotify::write_file('subscription', $info);
+       my $req_sockets = get_sockets();
+       check_key($key, $req_sockets);
+
+       my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+       my $proxy = $dccfg->{http_proxy};
+
+       $info = Proxmox::RS::Subscription::check_subscription($key, $server_id, "", "Proxmox VE", $proxy);
+
+       write_etc_subscription($info);
 
        return undef;
     }});
 
 __PACKAGE__->register_method ({
-    name => 'set', 
-    path => '', 
+    name => 'set',
+    path => '',
     method => 'PUT',
+    permissions => {
+       check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+    },
     description => "Set subscription key.",
     proxyto => 'node',
     protected => 1,
     parameters => {
-       additionalProperties => 0,
+       additionalProperties => 0,
        properties => {
            node => get_standard_option('pve-node'),
            key => {
                description => "Proxmox VE subscription key",
                type => 'string',
+               pattern => $subscription_pattern,
+               maxLength => 32,
            },
        },
     },
@@ -326,23 +225,52 @@ __PACKAGE__->register_method ({
     code => sub {
        my ($param) = @_;
 
+       my $key = PVE::Tools::trim($param->{key});
+
        my $info = {
            status => 'New',
-           key => $param->{key},
+           key => $key,
            checktime => time(),
        };
 
        my $req_sockets = get_sockets();
-       my $server_id = get_hwaddress();
+       my $server_id = PVE::API2Tools::get_hwaddress();
+
+       check_key($key, $req_sockets);
+
+       write_etc_subscription($info);
 
-       check_fields($info, $server_id, $req_sockets);
+       my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+       my $proxy = $dccfg->{http_proxy};
 
-       PVE::INotify::write_file('subscription', $info);
+       $info = Proxmox::RS::Subscription::check_subscription($key, $server_id, "", "Proxmox VE", $proxy);
 
-       $info = check_subscription($param->{key});
+       write_etc_subscription($info);
 
-       PVE::INotify::write_file('subscription', $info);
+       return undef;
+    }});
 
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '',
+    method => 'DELETE',
+    permissions => {
+       check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+    },
+    description => "Delete subscription key of this node.",
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => { type => 'null'},
+    code => sub {
+       my $subscription_file = '/etc/subscription';
+       return if ! -e $subscription_file;
+       unlink($subscription_file) or die "cannot delete subscription key: $!";
        return undef;
     }});