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);
use base qw(PVE::RESTHandler);
-PVE::INotify::register_file('subscription', "/etc/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();
}
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(validdirectory productname regdate nextduedate)) {
- if (!$info->{$f}) {
- die "Missing field '$f'\n";
- }
- }
-
- my $found;
- foreach my $hwid (split(/,/, $info->{validdirectory})) {
- if ($hwid 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();
+sub read_etc_subscription {
+ my $req_sockets = get_sockets();
+ my $server_id = PVE::API2Tools::get_hwaddress();
- check_fields($localinfo, $server_id, $req_sockets);
+ my $info = Proxmox::RS::Subscription::read_subscription($filename);
- my $age = time() - $localinfo->{checktime};
+ return $info if !$info || $info->{status} ne 'active';
- 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;
- }
- }
-
- 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 check_subscription {
- my ($key) = @_;
+sub write_etc_subscription {
+ my ($info) = @_;
- my $whmcsurl = "http://shop2.maurer-it.com";
+ 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 $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 $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
- my $proxy = $dccfg->{http_proxy};
-
- my $params = {
- licensekey => $key,
- dir => $server_id,
- domain => 'www.proxmox.com',
- 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'], timeout => 30);
-
- if ($proxy) {
- $ua->proxy(['http'], $proxy);
- } else {
- $ua->env_proxy;
- }
-
- 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";
- }
-
- 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";
- }
+ # 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"
}
-
- 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 => {
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,
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};
- PVE::INotify::write_file('subscription', $info);
+ # key has been recently checked
+ return undef
+ if !$param->{force}
+ && $info->{status} eq 'active'
+ && Proxmox::RS::Subscription::check_age($info, 1)->{status} eq 'active';
+
+ 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,
},
},
},
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);
- check_fields($info, $server_id, $req_sockets);
+ write_etc_subscription($info);
- PVE::INotify::write_file('subscription', $info);
+ my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ my $proxy = $dccfg->{http_proxy};
- $info = check_subscription($param->{key});
+ $info = Proxmox::RS::Subscription::check_subscription($key, $server_id, "", "Proxmox VE", $proxy);
- PVE::INotify::write_file('subscription', $info);
+ write_etc_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;
}});