diff --git a/.gitignore b/.gitignore index 07ee706..3ca5eff 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ dist build eggs parts -bin var sdist develop-eggs @@ -40,4 +39,4 @@ output/*.html output/*/index.html # Sphinx -docs/_build \ No newline at end of file +docs/_build diff --git a/AUTHORS.rst b/AUTHORS.rst index a25e770..f558988 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,4 @@ Development Lead Contributors ------------ -None yet. Why not be the first? \ No newline at end of file +* Simon Arlott (TLS support) diff --git a/HISTORY.rst b/HISTORY.rst index bb7113e..02656f1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,11 @@ History ========= +0.9.0 (2020-07-17) +------------------ +* move host database management to a separate command +* add support for configuring TLS + 0.8.0 (2017-06-27) ------------------ * add support for hybernate power state (thanks Chen Rotem Levy) diff --git a/README.rst b/README.rst index 7a99c39..51cd285 100644 --- a/README.rst +++ b/README.rst @@ -69,24 +69,24 @@ One you do that, reboot and you are on your way. amtctrl ------- -The ``amt`` library installs a binary ``amtctrl`` for working with AMT -enabled machines. +The ``amt`` library installs binaries ``amtctrl`` and ``amthostdb`` for working +with AMT enabled machines. machine enrollment ~~~~~~~~~~~~~~~~~~ -To simplify the control commands ``amtcrtl`` has a machine +To simplify the control commands ``amthostdb`` has a machine registry. New machines are added via: - amtctrl add
+ amthostdb add
You can see a list of all machines with: - amtctrl list + amthostdb list And remove an existing machine with: - amtctrl rm + amthostdb rm controlling machines @@ -94,21 +94,101 @@ controlling machines Once machines are controlled you have a number of options exposed: - amtctrl + amtctrl [subcommand] [arguments] Command is one of: -* on - power on the machine +* power on - power on the machine -* off - power off the machine +* power off - power off the machine -* reboot - power cycle the machine +* power reboot - power cycle the machine + +* power status - return power status as an ugly CIM blob (TODO: make this better) * pxeboot - set the machine to pxeboot the next time it reboots, and reboot the machine. This is extremely useful if you have install automation on pxeboot. -* status - return power status as an ugly CIM blob (TODO: make this better) +* pki list certs - list PKI certificates + +* pki list keys - list PKI keys + +* pki add cert - add PKI certificate + +* pki add cert -t - add trusted PKI certificate + +* pki add key - add PKI RSA key + +* pki generate 2048 - generate 2048-bit PKI RSA key + +* pki request - sign a PKI CSR + +* pki rm cert - remove PKI certificate + +* pki rm key - remove PKI key + +* pki tls - configure TLS to use PKI key + +* time - set AMT system time + +* tls enable -r <-s|-p> [-m] [-c ] - configure and enable remote TLS + (with/without mutual authentication, with/without allowing plaintext) + +* tls enable -l - configure and enable local TLS + +* tls status - get current TLS settings + +* tls disable -r - disable remote TLS + +* tls disable -l - disable local TLS + +* uuid - get AMT system UUID + +* version - get AMT version + + +configuring TLS +~~~~~~~~~~~~~~~ + +The AMT supports 2048-bit keys for end-entity certificates and 4096-bit keys for +certificate authorities/intermediate certificates. It supports SHA512 hashes. + +Various actions will not work without taking appropriate steps: + + * TLS cannot be enabled until it is configured + * Certificates and keys in active use for TLS cannot be removed + (this includes all trusted certificates when mutual authentication is enabled) + +Client certificates must have extended key usage ``1.3.6.1.5.5.7.3.2`` +(TLS Web Client Authentication) and ``2.16.840.1.113741.1.2.1`` (Intel AMT Remote Console). + +Configuring the supported Common Names (``tls enable -c ... -c ... -c ...``) is optional. + +Repeatedly updating the certificate (e.g. using Let's Encrypt) may wear out the +AMT flash. Use your own root CA. + +Configuring a Certificate Recovation List is not supported by this application. + +Be careful not to prevent yourself from accessing the AMT while configuring TLS, +i.e. allow plaintext while making changes until TLS has been tested. + +1. Generate a key with ``amtctrl ... pki generate 2048`` +2. Get it with ``amtctrl ... pki list keys`` and save to ``amt_rsa_public_key.pem`` +3. Convert it to a generic public key with ``openssl rsa -RSAPublicKey_in -in amt_rsa_public_key.pem -pubout -out amt_public_key.pem`` +4. Create a CSR with ``openssl genrsa | openssl x509 -x509toreq -new -subj /CN=example.com -signkey /dev/stdin -force_pubkey amt_public_key.pem -out amt_csr.pem`` + (requires OpenSSL 3.0.0+) +5. Use ``amtctrl ... pki request amt_csr.pem `` to get the AMT to sign the CSR +6. Issue a certificate from your CA using the CSR +7. Import the certificate with ``amtctrl ... pki add cert amt_cert.pem`` +8. Configure the new certificate to be used with TLS with ``amtctrl ... pki tls `` +9. Enable TLS (allowing plaintext) with ``amtctrl ... tls enable -r -l -p`` +10. Test HTTPS access, using ``amthostdb`` to configure the root CA +11. Enable TLS (disallowing plaintext) with ``amtctrl ... tls enable -r -l -s`` +12. Use ``amtctrl ... pki add cert -t root_ca.pem`` to import the root CA for client authentication +13. Enable TLS (allowing plaintext) with ``amtctrl ... tls enable -r -l -p -m`` +14. Test HTTPS access, using ``amthostdb`` to configure the root CA, user key and user cert +15. Enable TLS (disallowing plaintext) with ``amtctrl ... tls enable -r -l -s -m`` Futures ------- @@ -117,10 +197,10 @@ Futures this) * Retry http requests when they fail. AMT processors randomly drop -some connections, built in limited retry should be done. + some connections, built in limited retry should be done. * Fault handling. The current code is *very* optimistic. Hence, the 0.x nature. -* Remove console control. There are AMT commands to expose a VNC +* Remote console control. There are AMT commands to expose a VNC remote console on the box. Want to support those. diff --git a/amt/__init__.py b/amt/__init__.py index 42b2b0a..91c0983 100755 --- a/amt/__init__.py +++ b/amt/__init__.py @@ -2,4 +2,4 @@ __author__ = 'Sean Dague' __email__ = 'sean@dague.net' -__version__ = '0.8.0' +__version__ = '0.9.0' diff --git a/amt/client.py b/amt/client.py index e650857..5be4869 100755 --- a/amt/client.py +++ b/amt/client.py @@ -23,6 +23,10 @@ import requests from requests.auth import HTTPDigestAuth +import pem +import time +import uuid + import amt.wsman @@ -41,15 +45,33 @@ CIM_BootService = SCHEMA_BASE + 'CIM_BootService' CIM_ComputerSystem = SCHEMA_BASE + 'CIM_ComputerSystem' +CIM_ComputerSystemPackage = SCHEMA_BASE + 'CIM_ComputerSystemPackage' CIM_BootConfigSetting = SCHEMA_BASE + 'CIM_BootConfigSetting' CIM_BootSourceSetting = SCHEMA_BASE + 'CIM_BootSourceSetting' +SCHEMA_BASE = 'http://intel.com/wbem/wscim/1/amt-schema/1/' + +AMT_PublicKeyManagementService = SCHEMA_BASE + 'AMT_PublicKeyManagementService' +AMT_PublicKeyCertificate = SCHEMA_BASE + 'AMT_PublicKeyCertificate' +AMT_PublicPrivateKeyPair = SCHEMA_BASE + 'AMT_PublicPrivateKeyPair' +AMT_TLSSettingData = SCHEMA_BASE + 'AMT_TLSSettingData' +AMT_TLSCredentialContext = SCHEMA_BASE + 'AMT_TLSCredentialContext' +AMT_SetupAndConfigurationService = SCHEMA_BASE + 'AMT_SetupAndConfigurationService' +AMT_TimeSynchronizationService = SCHEMA_BASE + 'AMT_TimeSynchronizationService' + +del SCHEMA_BASE + # Additional useful constants _SOAP_ENVELOPE = 'http://www.w3.org/2003/05/soap-envelope' +_SOAP_ENUMERATION = 'http://schemas.xmlsoap.org/ws/2004/09/enumeration' _ADDRESS = 'http://schemas.xmlsoap.org/ws/2004/08/addressing' _ANONYMOUS = 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous' _WSMAN = 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd' +_TLS_REMOTE = 'Intel(r) AMT 802.3 TLS Settings' +_TLS_LOCAL = 'Intel(r) AMT LMS TLS Settings' +_TLS_EP_COLLECTION = 'TLSProtocolEndpointInstances Collection' + # magic ports to connect to AMT_PROTOCOL_PORT_MAP = { @@ -71,47 +93,62 @@ class Client(object): """ def __init__(self, address, password, username='admin', protocol='http', - vncpasswd=None): + vncpasswd=None, ca=None, key=None, cert=None): port = AMT_PROTOCOL_PORT_MAP[protocol] - path = '/wsman' + self.path = '/wsman' self.uri = "%(protocol)s://%(address)s:%(port)s%(path)s" % { 'address': address, 'protocol': protocol, 'port': port, - 'path': path} + 'path': self.path} self.username = username self.password = password self.vncpassword = vncpasswd + self.session = requests.Session() + self.session.auth = HTTPDigestAuth(self.username, self.password) + self.session.verify = ca if ca is not None else False + if key is not None and cert is not None: + self.session.cert = (cert, key) def post(self, payload, ns=None): - resp = requests.post(self.uri, - headers={'content-type': - 'application/soap+xml;charset=UTF-8'}, - auth=HTTPDigestAuth(self.username, self.password), - data=payload) + resp = self.session.post(self.uri, + headers={'content-type': + 'application/soap+xml;charset=UTF-8'}, + data=payload) resp.raise_for_status() + self.version = resp.headers.get('Server') if ns: rv = _return_value(resp.content, ns) if rv == 0: return 0 print(pp_xml(resp.content)) + return rv else: - return 0 + return resp.content def power_on(self): """Power on the box.""" - payload = amt.wsman.power_state_request(self.uri, "on") - return self.post(payload, CIM_PowerManagementService) + payload = amt.wsman.power_state_request(self.path, "on") + self.post(payload, CIM_PowerManagementService) + return 0 def power_off(self): """Power off the box.""" - payload = amt.wsman.power_state_request(self.uri, "off") - return self.post(payload, CIM_PowerManagementService) + payload = amt.wsman.power_state_request(self.path, "off") + self.post(payload, CIM_PowerManagementService) + return 0 def power_cycle(self): """Power cycle the box.""" - payload = amt.wsman.power_state_request(self.uri, "reboot") - return self.post(payload, CIM_PowerManagementService) + payload = amt.wsman.power_state_request(self.path, "reboot") + self.post(payload, CIM_PowerManagementService) + return 0 + + def power_cycle_hard(self): + """Power cycle hard the box.""" + payload = amt.wsman.power_state_request(self.path, "reset") + self.post(payload, CIM_PowerManagementService) + return 0 def pxe_next_boot(self): """Sets the machine to PXE boot on its next reboot @@ -125,46 +162,212 @@ def set_next_boot(self, boot_device): Will default back to normal boot list on the reboot that follows. """ - payload = amt.wsman.change_boot_order_request(self.uri, boot_device) + payload = amt.wsman.change_boot_order_request(self.path, boot_device) self.post(payload) - payload = amt.wsman.enable_boot_config_request(self.uri) + payload = amt.wsman.enable_boot_config_request(self.path) self.post(payload) def power_status(self): payload = amt.wsman.get_request( - self.uri, + self.path, CIM_AssociatedPowerManagementService) - resp = requests.post(self.uri, - auth=HTTPDigestAuth(self.username, self.password), - data=payload) - resp.raise_for_status() + resp = self.post(payload) value = _find_value( - resp.content, + resp, CIM_AssociatedPowerManagementService, "PowerState") return value + def get_pki_certs(self): + certs = self._enum_values(AMT_PublicKeyCertificate) + return [_xml_to_dict(cert) for cert in certs] + + def add_pki_cert(self, filename, trusted): + certs = [] + for cert in pem.parse_file(filename): + content = "".join(cert.as_text().splitlines()[1:-1]) + resp = self.post(amt.wsman.add_cert(self.path, content, trusted)) + rv = _return_value(resp, AMT_PublicKeyManagementService) + selector = _find_node(resp, _WSMAN, "Selector") + certs.append((rv, None if selector is None else selector.text)) + return certs + + def remove_pki_cert(self, selector): + resp = self.post(amt.wsman.delete_item(self.path, AMT_PublicKeyCertificate, "InstanceID", selector)) + return _find_value(resp, _ADDRESS, "Action") == "http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse" + + def get_pki_keys(self): + keys = self._enum_values(AMT_PublicPrivateKeyPair) + return [_xml_to_dict(key) for key in keys] + + def add_pki_key(self, filename): + keys = [] + for key in pem.parse_file(filename): + content = "".join(key.as_text().splitlines()[1:-1]) + resp = self.post(amt.wsman.add_key(self.path, content)) + rv = _return_value(resp, AMT_PublicKeyManagementService) + selector = _find_node(resp, _WSMAN, "Selector") + keys.append((rv, None if selector is None else selector.text)) + return keys + + def generate_pki_key(self, bits): + resp = self.post(amt.wsman.generate_key(self.path, bits)) + rv = _return_value(resp, AMT_PublicKeyManagementService) + selector = _find_node(resp, _WSMAN, "Selector") + return (rv, None if selector is None else selector.text) + + def sign_pki_csr(self, filename, selector): + requests = [] + for request in pem.parse_file(filename): + content = "".join(request.as_text().splitlines()[1:-1]) + resp = self.post(amt.wsman.sign_pki_csr(self.path, content, "InstanceID", selector)) + rv = _return_value(resp, AMT_PublicKeyManagementService) + signed_request = _find_node(resp, AMT_PublicKeyManagementService, "SignedCertificateRequest") + requests.append((rv, None if signed_request is None else signed_request.text)) + return requests + + def remove_pki_key(self, selector): + resp = self.post(amt.wsman.delete_item(self.path, AMT_PublicPrivateKeyPair, "InstanceID", selector)) + return _find_value(resp, _ADDRESS, "Action") == "http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse" + + def _format_tls_credentials(self, xml_creds): + creds = {} + for cred in xml_creds: + instance = cred.find('./{' + AMT_TLSCredentialContext + '}ElementProvidingContext//{' + _WSMAN + '}Selector') + if instance is not None: + instance = instance.text + creds[instance] = _xml_to_dict(cred) + return creds + + def get_tls_credentials(self): + return self._format_tls_credentials(self._enum_values(AMT_TLSCredentialContext)) + + def configure_tls_pki(self, instance): + exists = len(self._enum_values(AMT_TLSCredentialContext)) > 0 + + if instance == '': + if exists: + self.post(amt.wsman.delete_item(self.path, AMT_TLSCredentialContext, None, None)) + return exists + + creds = amt.wsman.prepare_tls_credentials(instance) + + if exists: + self.post(amt.wsman.put_item(self.path, AMT_TLSCredentialContext, None, None, creds)) + else: + self.post(amt.wsman.create_item(self.path, AMT_TLSCredentialContext, creds)) + + return True + + def _enable_tls(self, instance, plaintext, mutual, cn): + resp = self.post(amt.wsman.get_item(self.path, AMT_TLSSettingData, "InstanceID", instance)) + config = _find_node(resp, AMT_TLSSettingData, "AMT_TLSSettingData") + _xml_set(config, AMT_TLSSettingData, "Enabled", ["true"]) + _xml_set(config, AMT_TLSSettingData, "AcceptNonSecureConnections", plaintext) + _xml_set(config, AMT_TLSSettingData, "MutualAuthentication", mutual) + if cn is not None: + _xml_set(config, AMT_TLSSettingData, "TrustedCN", cn) + self.post(amt.wsman.put_item(self.path, AMT_TLSSettingData, "InstanceID", instance, config)) + return True + + def enable_remote_tls(self, plaintext, mutual, cn): + if mutual: + self.set_time() + return self._enable_tls(_TLS_REMOTE, [str(plaintext).lower()], [str(mutual).lower()], cn) + + def enable_local_tls(self): + return self._enable_tls(_TLS_LOCAL, ["true"], ["false"], None) + + def get_tls_status(self): + types = { + _TLS_REMOTE: "remote", + _TLS_LOCAL: "local", + } + settings = {} + for setting in self._enum_values(AMT_TLSSettingData): + instance = setting.find('./{' + AMT_TLSSettingData + '}InstanceID') + if instance is not None: + instance = instance.text + settings[types.get(instance)] = _xml_to_dict(setting) + return settings + + def _disable_tls(self, instance): + resp = self.post(amt.wsman.get_item(self.path, AMT_TLSSettingData, "InstanceID", instance)) + config = _find_node(resp, AMT_TLSSettingData, "AMT_TLSSettingData") + _xml_set(config, AMT_TLSSettingData, "Enabled", ["false"]) + resp = self.post(amt.wsman.put_item(self.path, AMT_TLSSettingData, "InstanceID", instance, config)) + return _xml_to_dict(_find_node(resp, AMT_TLSSettingData, "AMT_TLSSettingData")) + + def disable_remote_tls(self): + return self._disable_tls(_TLS_REMOTE) + + def disable_local_tls(self): + return self._disable_tls(_TLS_LOCAL) + + def commit_setup_changes(self): + return self.post(amt.wsman.commit_setup_changes(self.path), AMT_SetupAndConfigurationService) + + def set_time(self): + remote_reference_time = int(time.time()) + resp = self.post(amt.wsman.get_time(self.path)) + local_reference_time = int(_find_value(resp, AMT_TimeSynchronizationService, "Ta0")) + + remote_current_time = int(time.time()) + resp = self.post(amt.wsman.set_time(self.path, local_reference_time, remote_reference_time, remote_current_time)) + + return {"old": local_reference_time, "new": remote_current_time, "rv": _return_value(resp, AMT_TimeSynchronizationService)} + + def get_uuid(self): + resp = self.post(amt.wsman.get_request(self.path, CIM_ComputerSystemPackage)) + value = _find_value(resp, CIM_ComputerSystemPackage, "PlatformGUID") + return uuid.UUID(value) + + def get_version(self): + self.get_uuid() + return self.version + def enable_vnc(self): if self.vncpassword is None: print("VNC Password was not set") return False - payload = amt.wsman.enable_remote_kvm(self.uri, self.vncpassword) + payload = amt.wsman.enable_remote_kvm(self.path, self.vncpassword) self.post(payload) - payload = amt.wsman.kvm_redirect(self.uri) + payload = amt.wsman.kvm_redirect(self.path) self.post(payload) return True def vnc_status(self): payload = amt.wsman.get_request( - self.uri, + self.path, ('http://intel.com/wbem/wscim/1/ips-schema/1/' 'IPS_KVMRedirectionSettingData')) - resp = requests.post(self.uri, - auth=HTTPDigestAuth(self.username, self.password), - data=payload) - resp.raise_for_status() - return pp_xml(resp.content) + return pp_xml(self.post(payload)) + + def _enum_values(self, resource): + values = [] + + resp = self.post(amt.wsman.enumerate_begin(self.path, resource)) + context = _find_value(resp, _SOAP_ENUMERATION, "EnumerationContext") + + while context: + resp = self.post(amt.wsman.enumerate_next(self.path, resource, context)) + items = _find_node(resp, _SOAP_ENUMERATION, "Items") + for item in items: + values.append(item) + + eos = _find_node(resp, _SOAP_ENUMERATION, "EndOfSequence") + if eos is not None: + context = None + + return values + + +def _find_node(content, ns, key): + """Find the return value in a response.""" + doc = ElementTree.fromstring(content) + query = './/{%(ns)s}%(item)s' % {'ns': ns, 'item': key} + return doc.find(query) def _find_value(content, ns, key): @@ -173,10 +376,7 @@ def _find_value(content, ns, key): The xmlns is needed because everything in CIM is a million levels of namespace indirection. """ - doc = ElementTree.fromstring(content) - query = './/{%(ns)s}%(item)s' % {'ns': ns, 'item': key} - rv = doc.find(query) - return rv.text + return _find_node(content, ns, key).text def _return_value(content, ns): @@ -185,7 +385,25 @@ def _return_value(content, ns): The xmlns is needed because everything in CIM is a million levels of namespace indirection. """ - doc = ElementTree.fromstring(content) - query = './/{%(ns)s}%(item)s' % {'ns': ns, 'item': 'ReturnValue'} - rv = doc.find(query) - return int(rv.text) + rv = _find_node(content, ns, 'ReturnValue') + return None if rv is None else int(rv.text) + + +def _xml_to_dict(elements): + data = {} + for key, value in [(element.tag.rpartition("}")[2], (_xml_to_dict(element) if element else element.text)) for element in elements]: + if key in data: + if type(data[key]) != list: + data[key] = [data[key]] + data[key].append(value) + else: + data[key] = value + return data + + +def _xml_set(elements, ns, tag, values): + for element in elements.findall('./{%(ns)s}%(item)s' % {'ns': ns, 'item': tag}): + elements.remove(element) + for value in values: + element = ElementTree.SubElement(elements, '{%(ns)s}%(item)s' % {'ns': ns, 'item': tag}) + element.text = value diff --git a/amt/hostdb.py b/amt/hostdb.py index f22fe19..388cbb3 100644 --- a/amt/hostdb.py +++ b/amt/hostdb.py @@ -31,15 +31,31 @@ def list_servers(self): for item in self.config.sections(): print(" %s" % item) - def set_server(self, name, host, passwd, vncpasswd=None): + def set_server(self, name, host, passwd, vncpasswd=None, scheme='http', + ca=None, key=None, cert=None): # This is add/update if not self.config.has_section(name): self.config.add_section(name) self.config.set(name, 'host', host) self.config.set(name, 'passwd', passwd) + self.config.set(name, 'scheme', scheme) if vncpasswd is not None: self.config.set(name, 'vncpasswd', vncpasswd) + + if ca is not None: + self.config.set(name, 'ca', ca) + else: + self.config.remove_option(name, 'ca') + if key is not None: + self.config.set(name, 'key', key) + else: + self.config.remove_option(name, 'key') + if cert is not None: + self.config.set(name, 'cert', cert) + else: + self.config.remove_option(name, 'cert') + # ensure the directory exists if not os.path.exists(self.confdir): os.makedirs(self.confdir, 0o770) @@ -63,6 +79,13 @@ def get_server(self, name): data['vncpasswd'] = self.config.get(name, 'vncpasswd') else: data['vncpasswd'] = None + if self.config.has_option(name, 'scheme'): + data['scheme'] = self.config.get(name, 'scheme') + else: + data['scheme'] = 'http' + data['ca'] = self.config.get(name, 'ca', fallback=None) + data['key'] = self.config.get(name, 'key', fallback=None) + data['cert'] = self.config.get(name, 'cert', fallback=None) return data else: print("No config found for server (%s), " diff --git a/amt/wsman.py b/amt/wsman.py index 8776fb9..b1dcf0b 100644 --- a/amt/wsman.py +++ b/amt/wsman.py @@ -21,6 +21,7 @@ # not straight forward to build, so the code is hard to test, and # quite non portable. +from xml.etree import ElementTree import uuid POWER_STATES = { @@ -28,6 +29,7 @@ 'off': 8, 'standby': 4, 'reboot': 5, + 'reset': 10, 'hibernate': 7, } @@ -40,37 +42,410 @@ FRIENDLY_POWER_STATE = {v: k for (k, v) in POWER_STATES.items()} +ElementTree.register_namespace("s", "http://www.w3.org/2003/05/soap-envelope") +ElementTree.register_namespace("wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing") +ElementTree.register_namespace("wsman", "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd") + def friendly_power_state(state): return FRIENDLY_POWER_STATE.get(int(state), 'unknown') def get_request(uri, resource): - stub = """ + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Get + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def enumerate_begin(uri, resource): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def enumerate_next(uri, resource, context): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/09/enumeration}EnumerationContext').text = context + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def add_cert(uri, content, trusted): + name = "AddTrustedRootCertificate" if trusted else "AddCertificate" + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService/""" + name + """ + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService}CertificateBlob').text = content + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def add_key(uri, content): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService/AddKey + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService}KeyBlob').text = content + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def generate_key(uri, bits): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService/GenerateKeyPair + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + 0 + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService}KeyLength').text = str(bits) + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def sign_pki_csr(uri, request, selector_name, selector_value): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService/GeneratePKCS10RequestEx + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicPrivateKeyPair + + + + 1 + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').set("Name", selector_name) + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = selector_value + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyManagementService}NullSignedCertificateRequest').text = request + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def prepare_tls_credentials(instance): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_PublicKeyCertificate + + + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TLSProtocolEndpointCollection + + TLSProtocolEndpoint Instances Collection + + + + +""") # noqa + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TLSCredentialContext}ElementInContext//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = instance + return xml + + +def commit_setup_changes(uri): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService/CommitChanges + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_SetupAndConfigurationService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def get_item(uri, resource, selector_name, selector_value): + xml = ElementTree.fromstring(""" http://schemas.xmlsoap.org/ws/2004/09/transfer/Get - %(uri)s - %(resource)s - uuid:%(uuid)s + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').set("Name", selector_name) + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = selector_value + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def create_item(uri, resource, content): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + xml.find('.//{http://www.w3.org/2003/05/soap-envelope}Body').append(content) + return ElementTree.tostring(xml) + + +def put_item(uri, resource, selector_name, selector_value, content): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Put + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + -""" # noqa - return stub % {'uri': uri, 'resource': resource, 'uuid': uuid.uuid4()} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + if selector_name is None: + header = xml.find('.//{http://www.w3.org/2003/05/soap-envelope}Header') + selector_set = header.find('./{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}SelectorSet') + header.remove(selector_set) + else: + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').set("Name", selector_name) + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = selector_value + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + xml.find('.//{http://www.w3.org/2003/05/soap-envelope}Body').append(content) + return ElementTree.tostring(xml) + + +def delete_item(uri, resource, selector_name, selector_value): + xml = ElementTree.fromstring(""" + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete + + + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}ResourceURI').text = resource + if selector_name is None: + header = xml.find('.//{http://www.w3.org/2003/05/soap-envelope}Header') + selector_set = header.find('./{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}SelectorSet') + header.remove(selector_set) + else: + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').set("Name", selector_name) + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = selector_value + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def get_time(uri): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService/GetLowAccuracyTimeSynch + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) + + +def set_time(uri, local_reference_time, remote_reference_time, remote_current_time): + xml = ElementTree.fromstring(""" + + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService/SetHighAccuracyTimeSynch + + http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + + + + + + + + + +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService}Ta0').text = str(local_reference_time) + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService}Tm1').text = str(remote_reference_time) + xml.find('.//{http://intel.com/wbem/wscim/1/amt-schema/1/AMT_TimeSynchronizationService}Tm2').text = str(remote_current_time) + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) def enable_remote_kvm(uri, passwd): - stub = """ + xml = ElementTree.fromstring(""" http://schemas.xmlsoap.org/ws/2004/09/transfer/Put -%(uri)s + http://intel.com/wbem/wscim/1/ips-schema/1/IPS_KVMRedirectionSettingData -uuid:%(uuid)s + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous @@ -83,22 +458,25 @@ def enable_remote_kvm(uri, passwd): Intel(r) KVM Redirection Settings true false -%(passwd)s + 0 -""" # noqa - return stub % {'uri': uri, 'passwd': passwd, 'uuid': uuid.uuid4()} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://intel.com/wbem/wscim/1/ips-schema/1/IPS_KVMRedirectionSettingData}RFBPassword').text = passwd + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) def kvm_redirect(uri): - stub = """ + xml = ElementTree.fromstring(""" http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_KVMRedirectionSAP/RequestStateChange -%(uri)s + http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_KVMRedirectionSAP -uuid:%(uuid)s + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous @@ -107,18 +485,20 @@ def kvm_redirect(uri): 2 -""" # noqa - return stub % {'uri': uri, 'uuid': uuid.uuid4()} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) def power_state_request(uri, power_state): - stub = """ + xml = ElementTree.fromstring(""" http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService/RequestPowerStateChange - %(uri)s + http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService - uuid:%(uuid)s + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous @@ -128,7 +508,7 @@ def power_state_request(uri, power_state): - %(power_state)d + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous @@ -140,10 +520,11 @@ def power_state_request(uri, power_state): -""" # noqa - return stub % {'uri': uri, - 'power_state': POWER_STATES[power_state], - 'uuid': uuid.uuid4()} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService}PowerState').text = str(POWER_STATES[power_state]) + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) def change_boot_to_pxe_request(uri): @@ -153,13 +534,13 @@ def change_boot_to_pxe_request(uri): def change_boot_order_request(uri, boot_device): assert boot_device in BOOT_DEVICES - stub = """ + xml = ElementTree.fromstring(""" http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootConfigSetting/ChangeBootOrder -%(uri)s + http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootConfigSetting -uuid:%(uuid)s + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous @@ -174,24 +555,26 @@ def change_boot_order_request(uri, boot_device): http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootSourceSetting - %(boot_device)s + -""" # noqa - return stub % {'uri': uri, 'uuid': uuid.uuid4(), - 'boot_device': BOOT_DEVICES[boot_device]} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd}Selector').text = BOOT_DEVICES[boot_device] + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) def enable_boot_config_request(uri): - stub = """ + xml = ElementTree.fromstring(""" http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootService/SetBootConfigRole -%(uri)s + http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootService -uuid:%(uuid)s + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous Intel(r) AMT Boot Service @@ -210,8 +593,10 @@ def enable_boot_config_request(uri): 1 -""" # noqa - return stub % {'uri': uri, 'uuid': uuid.uuid4()} +""") # noqa + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}To').text = uri + xml.find('.//{http://schemas.xmlsoap.org/ws/2004/08/addressing}MessageID').text = "uuid:" + str(uuid.uuid4()) + return ElementTree.tostring(xml) # Local Variables: diff --git a/bin/amtctrl b/bin/amtctrl index 42a8933..84f714e 100755 --- a/bin/amtctrl +++ b/bin/amtctrl @@ -2,6 +2,7 @@ import argparse import os +from pprint import pprint import sys import requests @@ -10,112 +11,86 @@ import amt.client import amt.hostdb import amt.wsman -RESERVE_WORDS = ['list', 'get', 'add', 'set', 'rm'] +def main(): + parser = argparse.ArgumentParser(description="amtctrl lets you do hardware control on AMT enabled Intel machines") -def parse_args(): - parser = argparse.ArgumentParser( - 'amtctrl', - formatter_class=argparse.RawDescriptionHelpFormatter, - description=(""" -amtctrl lets you do hardware control on AMT enabled Intel machines. To -make it easier to use interactively, it operates as 2 things, a host -database to register hosts by ip or hostname, and then runs commands -by those aliases. + parser.add_argument('-p', '--prompt-pass', + dest='prompt', action='store_true', + default=False, + help='Prompt for password, bypass database') + parser.add_argument('server', metavar='name', help='server name') + subparsers = parser.add_subparsers(dest='command') -Host DB Commands ----------------- + parser_power = subparsers.add_parser('power', help='get/set power state') + parser_power.add_argument('state', choices=['on', 'off', 'reboot', 'reset', 'sleep', 'status']) -amtctrl list - list all servers registered -amtctrl set [-V vncpasswd] - register a server -amtctrl rm - unregister a server -amtctrl get - return info for the server + parser_pki = subparsers.add_parser('pki', help='configure PKI') -Control Commands ----------------- + pki_subparsers = parser_pki.add_subparsers(dest='subcommand', required=True) -amtctrl - run an amt command on the server + parser_pki_list = pki_subparsers.add_parser('list', help='list certificates/keys') + pki_list_subparsers = parser_pki_list.add_subparsers(dest='type', required=True) + parser_pki_list_certs = pki_list_subparsers.add_parser('certs', help='certificates') + parser_pki_list_keys = pki_list_subparsers.add_parser('keys', help='private keys') -command is one of: + parser_pki_add = pki_subparsers.add_parser('add', help='add certificate/key') + pki_add_subparsers = parser_pki_add.add_subparsers(dest='type', required=True) + parser_pki_add_cert = pki_add_subparsers.add_parser('cert', help='certificate') + parser_pki_add_cert.add_argument('-t', '--trusted', action='store_true', help='trusted root certificate') + parser_pki_add_cert.add_argument('filename', metavar='filename', help='certificate file (PEM format)') + parser_pki_add_key = pki_add_subparsers.add_parser('key', help='private key') + parser_pki_add_key.add_argument('filename', metavar='filename', help='RSA private key file (PEM format)') - on - power on - off - power off - reboot - reboot - pxeboot - reboot the machine and pxeboot on the next reboot cycle - status - dump cim power status - vnc - enable vnc on the server -""")) + parser_pki_rm = pki_subparsers.add_parser('rm', help='remove certificate/key') + pki_rm_subparsers = parser_pki_rm.add_subparsers(dest='type', required=True) + parser_pki_rm_cert = pki_rm_subparsers.add_parser('cert', help='certificate') + parser_pki_rm_cert.add_argument('id', metavar='InstanceID', help='certificate InstanceID') + parser_pki_rm_key = pki_rm_subparsers.add_parser('key', help='private key') + parser_pki_rm_key.add_argument('id', metavar='InstanceID', help='private key InstanceID') - parser.add_argument('server', metavar='name', - help='') - parser.add_argument('-p', '--prompt-pass', - dest='prompt', action='store_true', - default=False, - help='Prompt for password, bypass database') - parser.add_argument('command', metavar='command', nargs='?', - help='') - return parser.parse_known_args()[0] - - -def parse_args_set(): - parser = argparse.ArgumentParser('amtctrl') - parser.add_argument('op', metavar='set', - help='') - parser.add_argument('name', metavar='name', - help='') - parser.add_argument('host', metavar='host', - help='') - parser.add_argument('passwd', metavar='passwd', - help='') - parser.add_argument("-V", '--vncpasswd', metavar='vncpasswd', - help='') - return parser.parse_args() - - -def parse_args_rm(): - parser = argparse.ArgumentParser('amtctrl') - parser.add_argument('op', metavar='remove', - help='') - parser.add_argument('name', metavar='name', - help='') - return parser.parse_args() - - -def parse_args_get(): - parser = argparse.ArgumentParser('amtctrl') - parser.add_argument('op', metavar='get', - help='') - parser.add_argument('name', metavar='name', - help='') - return parser.parse_args() - - -def do_db_actions(args, db): - if args.server == 'list': - return db.list_servers() - elif args.server == 'add' or args.server == 'set': - if args.server == 'add': - print("WARNING: ``add`` command is deprecated, ``set`` " - "is prefered") - set_args = parse_args_set() - return db.set_server(set_args.name, set_args.host, set_args.passwd, - set_args.vncpasswd) - elif args.server == 'rm': - rm_args = parse_args_rm() - return db.rm_server(rm_args.name) - elif args.server == 'get': - get_args = parse_args_get() - server = db.get_server(get_args.name) - print("%s => %s" % (get_args.name, server['host'])) + parser_pki_generate = pki_subparsers.add_parser('generate', help='generate private key') + parser_pki_generate.add_argument('bits', type=int, choices=[2048], help='RSA key size in bits') + parser_pki_request = pki_subparsers.add_parser('request', help='sign certificate signing request') + parser_pki_request.add_argument('filename', metavar='filename', help='unsigned certificate signing request file (PEM format)') + parser_pki_request.add_argument('id', metavar='InstanceID', help='private key InstanceID') -def main(): - args = parse_args() - db = amt.hostdb.HostDB() + parser_time = subparsers.add_parser('time', help='get/set AMT time') + + parser_pki_tls = pki_subparsers.add_parser('tls', help='configure TLS use of PKI') + parser_pki_tls.add_argument('id', metavar='InstanceID', help='certificate InstanceID') - # if the "server" name is reserve word, run that command - if args.server in RESERVE_WORDS: - return do_db_actions(args, db) + parser_pxeboot = subparsers.add_parser('pxeboot', help='reboot the machine and pxeboot on the next reboot cycle') + + parser_tls = subparsers.add_parser('tls', help='configure TLS') + + tls_subparsers = parser_tls.add_subparsers(dest='subcommand', required=True) + + parser_tls_enable = tls_subparsers.add_parser('enable', help='enable TLS') + parser_tls_enable.add_argument('-l', '--local', action='store_true', help='Configure local connections only') + parser_tls_enable.add_argument('-r', '--remote', action='store_true', help='Configure remote connections only') + parser_tls_enable_group = parser_tls_enable.add_mutually_exclusive_group(required=False) + parser_tls_enable_group.add_argument('-s', '--require-secure', action='store_true', help='Require secure connections') + parser_tls_enable_group.add_argument('-p', '--allow-plaintext', action='store_true', help='Allow plaintext connections') + parser_tls_enable.add_argument('-m', '--mutual', action='store_true', help='Require mutual authentication') + parser_tls_enable.add_argument('-c', '--cn', default=[], action='append', help='Allowed common names') + + parser_tls_status = tls_subparsers.add_parser('status', help='get TLS settings') + + parser_tls_disable = tls_subparsers.add_parser('disable', help='disable TLS') + parser_tls_disable.add_argument('-l', '--local', action='store_true', help='Configure local connections only') + parser_tls_disable.add_argument('-r', '--remote', action='store_true', help='Configure remote connections only') + + parser_uuid = subparsers.add_parser('uuid', help='get machine uuid') + + parser_version = subparsers.add_parser('version', help='get AMT version') + + parser_vnc = subparsers.add_parser('vnc', help='get/set vnc state') + parser_vnc.add_argument('state', choices=['start', 'status']) + + args = parser.parse_args() + db = amt.hostdb.HostDB() if args.prompt: host = args.server @@ -127,38 +102,148 @@ def main(): else: server = db.get_server(args.server) if not server: - print("Server %s not found in hostdb" % args.server) + print('Server %s not found in hostdb' % args.server) return 1 host = server['host'] passwd = server['passwd'] + scheme = server['scheme'] vncpasswd = server['vncpasswd'] + ca = server['ca'] + key = server['key'] + cert = server['cert'] + client = amt.client.Client(host, passwd, vncpasswd=vncpasswd, + protocol=scheme, ca=ca, + key=key, cert=cert) - client = amt.client.Client(host, passwd, vncpasswd=vncpasswd) try: - if args.command == "on": - client.power_on() - elif args.command == "off": - client.power_off() - elif args.command == "reboot": - client.power_cycle() - elif args.command == "pxeboot": + if args.command == 'power': + if args.state == 'on': + client.power_on() + elif args.state == 'off': + client.power_off() + elif args.state == 'reboot': + client.power_cycle() + elif args.state == 'reset': + client.power_cycle_hard() + elif args.state == 'sleep': + client.power_sleep() + elif args.state == 'hibernate': + client.power_hibernate() + else: + print(amt.wsman.friendly_power_state(client.power_status())) + elif args.command == 'pki': + if args.subcommand == 'list': + if args.type == 'certs': + for cert in client.get_pki_certs(): + if "X509Certificate" in cert: + content = cert["X509Certificate"] + del cert["X509Certificate"] + pprint(cert, width=100) + print("-----BEGIN CERTIFICATE-----") + print(content) + print("-----END CERTIFICATE-----") + print() + elif args.type == 'keys': + for key in client.get_pki_keys(): + if "DERKey" in key: + content = key["DERKey"] + del key["DERKey"] + pprint(key) + print("-----BEGIN RSA PUBLIC KEY-----") + print(content) + print("-----END RSA PUBLIC KEY-----") + print() + elif args.subcommand == 'add': + if args.type == 'cert': + for cert in client.add_pki_cert(args.filename, args.trusted): + pprint(cert, width=100) + elif args.type == 'key': + for key in client.add_pki_key(args.filename): + pprint(key, width=100) + elif args.subcommand == 'rm': + if args.type == 'cert': + print(client.remove_pki_cert(args.id)) + elif args.type == 'key': + print(client.remove_pki_key(args.id)) + elif args.subcommand == 'generate': + print(client.generate_pki_key(args.bits)) + elif args.subcommand == 'request': + # This needs a "null signed" CSR containing the public key only. + # + # Convert the AMT raw RSA public key into a generic public key: + # openssl rsa -RSAPublicKey_in -in amt_rsa_public_key.pem -pubout -out amt_public_key.pem + # + # Create a CSR for common name "example.com", signed with a temporary new private key: + # openssl genrsa | openssl x509 -x509toreq -new -subj /CN=example.com -signkey /dev/stdin -force_pubkey amt_public_key.pem -out amt_csr.pem + # + # (OpenSSL 3.0.0-alpha6-dev) + for (rv, request) in client.sign_pki_csr(args.filename, args.id): + print(rv) + if request is not None: + print("-----BEGIN CERTIFICATE REQUEST-----") + print(request) + print("-----END CERTIFICATE REQUEST-----") + print() + elif args.subcommand == 'tls': + print(client.configure_tls_pki(args.id)) + print(client.commit_setup_changes()) + elif args.command == 'pxeboot': client.pxe_next_boot() client.power_cycle() - elif args.command == "status": - print(amt.wsman.friendly_power_state(client.power_status())) - elif args.command == "vnc": - if client.enable_vnc(): - print("VNC enabled on port 5900 with AMT password") - elif args.command == "vncstatus": - print(client.vnc_status()) + elif args.command == 'time': + print(client.set_time()) + elif args.command == 'tls': + if args.subcommand == 'enable': + changed = False + + if args.remote: + if not (args.allow_plaintext or args.require_secure): + raise Exception("Remote TLS configuration must be set to allow plaintext connections or require secure connections") + + config = client.enable_remote_tls(plaintext=args.allow_plaintext, mutual=args.mutual, cn=args.cn) + changed = True + pprint(("config", "remote", config)) + if args.local: + config = client.enable_local_tls() + changed = True + pprint(("config", "local", config)) + + if changed: + print(client.commit_setup_changes()) + elif args.subcommand == 'status': + for (name, cred) in client.get_tls_credentials().items(): + pprint(("credentials", name, cred), width=200) + for (type, setting) in client.get_tls_status().items(): + pprint(("config", type, setting)) + elif args.subcommand == 'disable': + changed = False + + if args.remote: + config = client.disable_remote_tls() + changed = True + pprint(("config", "remote", config)) + if args.local: + config = client.disable_local_tls() + changed = True + pprint(("config", "local", config)) + + if changed: + print(client.commit_setup_changes()) + elif args.command == 'uuid': + print(client.get_uuid()) + elif args.command == 'version': + print(client.get_version()) + elif args.command == 'vnc': + if args.state == 'start': + if client.enable_vnc(): + print('VNC enabled on port 5900 with AMT password') + else: + print(client.vnc_status()) else: - print(("Unknown command %s" - "try one of on, off, reboot, pxeboot, " - "status, vnc, vncstatus") - % args.command) + parser.error("No command specified") except requests.exceptions.HTTPError as e: - print("Error: %s" % e) + print('Error: %s' % e) -if __name__ == "__main__": +if __name__ == '__main__': sys.exit(main()) diff --git a/bin/amthostdb b/bin/amthostdb new file mode 100755 index 0000000..cf10055 --- /dev/null +++ b/bin/amthostdb @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import argparse +import os +from pprint import pprint +import sys + +import requests + +import amt.client +import amt.hostdb +import amt.wsman + + +def main(): + parser = argparse.ArgumentParser(description="amthostdb lets you configure the host database for amtctrl") + subparsers = parser.add_subparsers(dest='command') + + parser_list = subparsers.add_parser('list', help='list all servers registered') + + parser_get = subparsers.add_parser('get', help='return info for a server') + parser_get.add_argument('server', metavar='name', help='server name') + + parser_set = subparsers.add_parser('set', help='register a server') + parser_set.add_argument('server', metavar='name', help='server name') + parser_set.add_argument('host', metavar='host') + parser_set.add_argument('passwd', metavar='passwd') + parser_set.add_argument('-S', '--scheme', metavar='scheme', default='http', choices=['http', 'https']) + parser_set.add_argument('--tls-ca', metavar='filename') + parser_set.add_argument('--tls-key', metavar='filename') + parser_set.add_argument('--tls-cert', metavar='filename') + parser_set.add_argument('-V', '--vncpasswd', metavar='vncpasswd') + + parser_rm = subparsers.add_parser('rm', help='unregister a server') + parser_rm.add_argument('server', metavar='name', help='server name') + + args = parser.parse_args() + db = amt.hostdb.HostDB() + + try: + if args.command == 'list': + db.list_servers() + elif args.command == 'get': + server = db.get_server(args.server) + print('%s => %s' % (args.server, server['host'])) + elif args.command == 'set': + db.set_server(args.server, args.host, args.passwd, + args.vncpasswd, scheme=args.scheme, + ca=args.tls_ca, key=args.tls_key, + cert=args.tls_cert) + elif args.command == 'rm': + db.rm_server(args.server) + else: + parser.error("No command specified") + except requests.exceptions.HTTPError as e: + print('Error: %s' % e) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt index 199f4cb..98cf5ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ appdirs +pem requests six diff --git a/setup.py b/setup.py index 2e1902a..4de41d3 100755 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ requirements = [ 'appdirs', + 'pem', 'requests', 'six', ] @@ -27,7 +28,7 @@ setup( name='amt', - version='0.8.0', + version='0.9.0', description="Tools for interacting with Intel's AMT", long_description=readme + '\n\n' + history, author="Sean Dague", @@ -38,7 +39,7 @@ ], package_dir={'amt': 'amt'}, - scripts=['bin/amtctrl'], + scripts=['bin/amtctrl', 'bin/amthostdb'], include_package_data=True, install_requires=requirements, license="Apache", diff --git a/tests/test_wsman.py b/tests/test_wsman.py index cd8b8f0..242e780 100755 --- a/tests/test_wsman.py +++ b/tests/test_wsman.py @@ -9,6 +9,7 @@ import mock import testtools +from xml.etree import ElementTree from amt import wsman @@ -19,8 +20,8 @@ def fake_uuid4(): class BaseTestCase(testtools.TestCase): def assertXmlEqual(self, one, two): - one = one.strip() - two = two.strip() + one = ElementTree.tostring(ElementTree.fromstring(one)) + two = ElementTree.tostring(ElementTree.fromstring(two)) array1 = [x.strip() for x in str(one).split("\n")] array2 = [x.strip() for x in str(two).split("\n")] self.maxDiff = None @@ -49,7 +50,7 @@ def test_get_request(self): """ # noqa - self.assertXmlEqual(wsman.get_request(uri, res), shouldbe) + self.assertXmlEqual(shouldbe, wsman.get_request(uri, res)) @mock.patch('uuid.uuid4', fake_uuid4) def test_change_boot_to_pxe_request(self): @@ -66,7 +67,7 @@ def test_change_boot_to_pxe_request(self): http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous - Intel(r) AMT: Boot Configuration 0 + Intel(r) AMT: Force PXE Boot @@ -76,14 +77,14 @@ def test_change_boot_to_pxe_request(self): http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_BootSourceSetting - Intel(r) AMT: Force PXE Boot + """ # noqa - self.assertXmlEqual(wsman.change_boot_to_pxe_request(uri), shouldbe) + self.assertXmlEqual(shouldbe, wsman.change_boot_to_pxe_request(uri)) def test_change_boot_order_request_invalid_boot_device(self): uri = 'http://10.42.0.50:16992/wsman'