diff --git a/cloudinit/osys/base.py b/cloudinit/osys/base.py new file mode 100644 index 00000000..0a468d89 --- /dev/null +++ b/cloudinit/osys/base.py @@ -0,0 +1,77 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import importlib +import platform + +import six + + +__all__ = ( + 'get_osutils', + 'OSUtils', +) + + +def get_osutils(): + """Obtain the OS utils object for the underlying platform.""" + name, _, _ = platform.linux_distribution() + if not name: + name = platform.system() + + name = name.lower() + location = "cloudinit.osys.{0}.base".format(name) + module = importlib.import_module(location) + return module.OSUtils + + +@six.add_metaclass(abc.ABCMeta) +class OSUtils(object): + """Base class for an OS utils namespace. + + This base class provides a couple of hooks which needs to be + implemented by subclasses, for each particular OS and distro. + """ + + name = None + + @abc.abstractproperty + def network(self): + """Get the network object for the underlying platform.""" + + @abc.abstractproperty + def filesystem(self): + """Get the filesystem object for the underlying platform.""" + + @abc.abstractproperty + def users(self): + """Get the users object for the underlying platform.""" + + @abc.abstractproperty + def general(self): + """Get the general object for the underlying platform.""" + + @abc.abstractproperty + def user_class(self): + """Get the user class specific to this operating system.""" + + @abc.abstractproperty + def route_class(self): + """Get the route class specific to this operating system.""" + + @abc.abstractproperty + def interface_class(self): + """Get the interface class specific to this operating system.""" diff --git a/cloudinit/osys/general.py b/cloudinit/osys/general.py new file mode 100644 index 00000000..8553ccb8 --- /dev/null +++ b/cloudinit/osys/general.py @@ -0,0 +1,43 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class General(object): + """Base class for the general namespace. + + This class should contain common functions between all OSes, + which can't be grouped in a domain-specific namespace. + """ + + @abc.abstractmethod + def set_timezone(self, timezone): + """Change the timezone for the underlying platform. + + The `timezone` parameter should be a TZID timezone format, + e.g. 'Africa/Mogadishu' + """ + + @abc.abstractmethod + def set_locale(self, locale): + """Change the locale for the underlying platform.""" + + @abc.abstractmethod + def reboot(self): + pass diff --git a/cloudinit/osys/network.py b/cloudinit/osys/network.py new file mode 100644 index 00000000..bf09c437 --- /dev/null +++ b/cloudinit/osys/network.py @@ -0,0 +1,152 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + +from cloudinit import util + + +__all__ = ( + 'Network', + 'Route', + 'Interface', +) + + +@six.add_metaclass(abc.ABCMeta) +class Network(object): + """Base network class for network related utilities.""" + + @abc.abstractmethod + def routes(self): + """Get the list of the available routes.""" + + @abc.abstractmethod + def default_gateway(self): + """Get the default gateway, as a route object.""" + + @abc.abstractmethod + def interfaces(self): + """Get the list of the available interfaces.""" + + @abc.abstractmethod + def hosts(self): + """Get the list of the available hosts.""" + + @abc.abstractmethod + def set_hostname(self, hostname): + """Change the host name of the instance.""" + + @abc.abstractmethod + def set_static_network_config(self, adapter_name, address, netmask, + broadcast, gateway, dnsnameservers): + """Configure a new static network.""" + + +@six.add_metaclass(abc.ABCMeta) +class Route(object): + """Base class for routes.""" + + def __init__(self, destination, gateway, netmask, + interface, metric, + flags=None, refs=None, use=None, expire=None): + self.destination = destination + self.gateway = gateway + self.netmask = netmask + self.interface = interface + self.metric = metric + self.flags = flags + self.refs = refs + self.use = use + self.expire = expire + + @abc.abstractproperty + def is_static(self): + """Check if this route is static.""" + + @util.abstractclassmethod + def add(cls, route): + """Add a new route in the underlying OS. + + The `route` parameter should be an instance of :class:`Route`. + """ + + @util.abstractclassmethod + def delete(cls, route): + """Delete a route from the underlying OS. + + The `route` parameter should be an instance of :class:`Route`. + """ + + +@six.add_metaclass(abc.ABCMeta) +class Interface(object): + """Base class reprensenting an interface. + + It provides both attributes for retrieving interface information, + as well as methods for modifying the state of a route, such + as activating or deactivating it. + """ + + def __init__(self, name, mac, index=None, mtu=None, + dhcp_server=None, dhcp_enabled=None): + self._mtu = mtu + + self.name = name + self.index = index + self.mac = mac + self.dhcp_server = dhcp_server + self.dhcp_enabled = dhcp_enabled + + def __eq__(self, other): + return (self.mac == other.mac and + self.name == other.name and + self.index == other.index) + + @abc.abstractmethod + def _change_mtu(self, value): + """Change the mtu for the underlying interface.""" + + @util.abstractclassmethod + def from_name(cls, name): + """Get an instance of :class:`Interface` from an interface name. + + E.g. this should retrieve the 'eth0' interface:: + + >>> Interface.from_name('eth0') + """ + + @abc.abstractmethod + def up(self): + """Activate the current interface.""" + + @abc.abstractmethod + def down(self): + """Deactivate the current interface.""" + + @abc.abstractmethod + def is_up(self): + """Check if this interface is activated.""" + + @property + def mtu(self): + return self._mtu + + @mtu.setter + def mtu(self, value): + self._change_mtu(value) + self._mtu = value diff --git a/cloudinit/osys/users.py b/cloudinit/osys/users.py new file mode 100644 index 00000000..d2d5c679 --- /dev/null +++ b/cloudinit/osys/users.py @@ -0,0 +1,67 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + +from cloudinit import util + + +@six.add_metaclass(abc.ABCMeta) +class Users(object): + """Base class for user related operations.""" + + @abc.abstractmethod + def groups(self): + """Get a list of the groups available in the system.""" + + @abc.abstractmethod + def users(self): + """Get a list of the users available in the system.""" + + +@six.add_metaclass(abc.ABCMeta) +class Group(object): + """Base class for user groups.""" + + @util.abstractclassmethod + def create(cls, group_name): + """Create a new group with the given name.""" + + @abc.abstractmethod + def add(self, member): + """Add a new member to this group.""" + + +@six.add_metaclass(abc.ABCMeta) +class User(object): + """Base class for an user.""" + + @classmethod + def create(self, username, password, **kwargs): + """Create a new user.""" + + @abc.abstractmethod + def home(self): + """Get the user's home directory.""" + + @abc.abstractmethod + def ssh_keys(self): + """Get the ssh keys for this user.""" + + @abc.abstractmethod + def change_password(self, password): + """Change the password for this user.""" diff --git a/cloudinit/tests/osys/__init__.py b/cloudinit/tests/osys/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudinit/tests/osys/test_base.py b/cloudinit/tests/osys/test_base.py new file mode 100644 index 00000000..6f4e00b1 --- /dev/null +++ b/cloudinit/tests/osys/test_base.py @@ -0,0 +1,50 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +from cloudinit.osys import base + + +class TestOSUtils(unittest.TestCase): + + @mock.patch('importlib.import_module') + @mock.patch('platform.linux_distribution') + @mock.patch('platform.system') + def _test_getosutils(self, mock_system, + mock_linux_distribution, mock_import_module, + linux=False): + if linux: + os_name = 'Linux' + else: + os_name = 'Windows' + + mock_system.return_value = os_name + mock_linux_distribution.return_value = (os_name, None, None) + module = base.get_osutils() + + mock_import_module.assert_called_once_with( + "cloudinit.osys.{0}.base".format(os_name.lower())) + self.assertEqual(mock_import_module.return_value.OSUtils, + module) + + def test_getosutils(self): + self._test_getosutils(linux=True) + self._test_getosutils(linux=False) diff --git a/cloudinit/util.py b/cloudinit/util.py new file mode 100644 index 00000000..5986ed7e --- /dev/null +++ b/cloudinit/util.py @@ -0,0 +1,26 @@ +# Copyright (C) 2015 Canonical Ltd. +# Copyright (C) 2015 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +__all__ = ('abstractclassmethod', ) + + +class abstractclassmethod(classmethod): + """A backport for abc.abstractclassmethod from Python 3.""" + + __isabstractmethod__ = True + + def __init__(self, func): + func.__isabstractmethod__ = True + super(abstractclassmethod, self).__init__(func)