def from_response(cls, body, identity): """ Create a host object from an API response. :param body: The raw API response text. :param identity: This host's identity. :return: An object representing the host. :raises ValueError: If the response is empty or malformed. :raises RuntimeError: If the response indicates the API request failed. """ if not body: raise ValueError('Cannot construct host from empty response') try: root = cElementTree.fromstring('<root>' + body + '</root>') if root.find('status').text != 'success': message = root.find('statusmsg') raise RuntimeError( 'Response indicates failed API call: {0}'.format( message.text if message else 'unspecified error')) return Host(identity, root.find('hostname').text, root.find('ipaddress').text, root.find('vmstat').text == 'online', Resource.from_response(root.find('mem').text), Resource.from_response(root.find('hdd').text), Resource.from_response(root.find('bw').text), root.find('ipaddr').text.split(',')) except ParseError as e: raise ValueError('Host response is malformed: {0}'.format(e)) except AttributeError as e: raise ValueError( 'Host response is missing an attribute: {0}'.format(e))
class TestHost(unittest.TestCase): _VENDOR_ENDPOINT = 'https://vpscp.ramnode.com' IDENTITY = HostIdentity('host-name', 'host-key', 'host-hash', Vendor('vendor-name', _VENDOR_ENDPOINT)) _FQDN = 'host.example.com' _PRIMARY_IP = '8.8.8.8' _IS_ONLINE = True _MEMORY = Resource(100, 100) _STORAGE = Resource(200, 200) _BANDWIDTH = Resource(300, 300) _IP_ADDRESSES = [_PRIMARY_IP, '10.10.10.10'] _XML = '''<ipaddr>{0}</ipaddr> <hdd>200,100,100,50</hdd> <bw>400,200,200,50</bw> <mem>600,300,300,50</mem> <status>{1}</status> <statusmsg></statusmsg> <hostname>{2}</hostname> <ipaddress>{3}</ipaddress> <vmstat>{4}</vmstat>''' _XML_VALID = _XML.format(','.join(_IP_ADDRESSES), 'success', _FQDN, _PRIMARY_IP, 'online' if _IS_ONLINE else 'offline') HOST = Host(IDENTITY, _FQDN, _PRIMARY_IP, _IS_ONLINE, _MEMORY, _STORAGE, _BANDWIDTH, _IP_ADDRESSES) @classmethod def setUpClass(cls): cls.host = Host(cls.IDENTITY, cls._FQDN, cls._PRIMARY_IP, cls._IS_ONLINE, cls._MEMORY, cls._STORAGE, cls._BANDWIDTH, cls._IP_ADDRESSES) @staticmethod def _make_host(identity=IDENTITY, fqdn=_FQDN, primary_ip=_PRIMARY_IP, is_online=_IS_ONLINE, memory=_MEMORY, storage=_STORAGE, bandwidth=_BANDWIDTH, ip_addresses=_IP_ADDRESSES): return Host(identity, fqdn, primary_ip, is_online, memory, storage, bandwidth, ip_addresses) @staticmethod def add_response(verb=responses.GET, url=_VENDOR_ENDPOINT + '/api/client/command.php', body=_XML_VALID, status=200): responses.add(verb, url, body, status=status) def test_action_none(self): with self.assertRaises(ValueError): self.host.action(None) def test_action_invalid(self): with self.assertRaises(ValueError): self.host.action('invalid') def test_boot(self): with patch.object(Host, 'action') as action: self.host.boot() action.assert_called_once_with('boot') def test_reboot(self): with patch.object(Host, 'action') as action: self.host.reboot() action.assert_called_once_with('reboot') def test_shutdown(self): with patch.object(Host, 'action') as action: self.host.shutdown() action.assert_called_once_with('shutdown') @responses.activate def test_request_from_identity_denied(self): self.add_response(status=403) with self.assertRaises(RuntimeError): Host.request_from_identity(self.IDENTITY) @responses.activate def test_request_from_identity(self): responses.add(responses.GET, self._VENDOR_ENDPOINT + '/api/client/command.php', status=200, body=self._XML_VALID) self.assertEqual(Host.request_from_identity(self.IDENTITY), self.host) def test_is_offline_false(self): self.assertFalse(self.host.is_offline) def test_is_offline_true(self): self.assertTrue(self._make_host(is_online=False).is_offline) def test_from_response_none(self): with self.assertRaises(ValueError): Host.from_response(None, self.IDENTITY) def test_from_response_malformed(self): with self.assertRaises(ValueError): Host.from_response('><', self.IDENTITY) def test_from_response_api_failure(self): with self.assertRaises(RuntimeError): Host.from_response( self._XML.format(','.join(self._IP_ADDRESSES), 'failure', self._FQDN, self._PRIMARY_IP, 'online' if self._IS_ONLINE else 'offline'), self.IDENTITY) def test_from_response_missing_attribute(self): with self.assertRaises(ValueError): Host.from_response('<root/>', self.IDENTITY) def test_from_response(self): self.assertEqual(Host.from_response(self._XML_VALID, self.IDENTITY), self.host) def test_str(self): self.assertEqual(str(self.host), '{0}({1})'.format(Host.__name__, self._FQDN))
def test_eq_false_free(self): self.assertNotEqual(self.resource, Resource(100, 100))
def test_str(self): resource = Resource(6155997184, 6728904704) self.assertEqual(str(resource), 'Resource(6155997184 bytes used (47.78%), 6728904704 ' 'bytes free (52.22%))')
def test_from_response(self): self.assertEqual(Resource(6155997184, 6728904704), Resource.from_response('12884901888,6155997184,' '6728904704,48'))
def test_eq_false_used(self): self.assertNotEqual(self.resource, Resource(50, 50))
def test_from_response_float(self): with self.assertRaises(ValueError): Resource.from_response('12884901.888,6155997184,6728904704,48')
def test_from_response_string(self): with self.assertRaises(ValueError): Resource.from_response('12884901888,surprise,6728904704,48')
def test_from_response_too_few_fragments(self): with self.assertRaises(ValueError): Resource.from_response('12884901888,6155997184,6728904704')
def test_from_response_none(self): with self.assertRaises(ValueError): Resource.from_response(None)
def test_zero_total_bytes(self): self.assertEqual(Resource(0, 0).used_percentage, 1)
def test_percentages_sum_to_one(self): resource = Resource(14682693209, 522188218791) self.assertEqual(resource.used_percentage + resource.free_percentage, 1)
def setUpClass(cls): cls.resource = Resource(cls._USED_BYTES, cls._FREE_BYTES)