From f2e01e18da7f186ee8b3dd53f3657745942d2b55 Mon Sep 17 00:00:00 2001 From: Kyle Hogan Date: Thu, 10 Mar 2016 09:19:06 -0500 Subject: [PATCH 1/3] update cfg example --- examples/haas.cfg.dev-no-hardware | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/haas.cfg.dev-no-hardware b/examples/haas.cfg.dev-no-hardware index 28ef8ccc..11e107a5 100644 --- a/examples/haas.cfg.dev-no-hardware +++ b/examples/haas.cfg.dev-no-hardware @@ -23,7 +23,7 @@ dry_run=True [extensions] haas.ext.switches.mock = -# haas.ext.network_allocators.null = +haas.ext.network_allocators.null = # # Depending on what you're trying to do, you may want to use the vlan_pool # network allocator instead of the null allocator. To do this, comment out the From 55a8dcf1218e3d59984975d1d20cf42c3371a11e Mon Sep 17 00:00:00 2001 From: Kyle Hogan Date: Thu, 10 Mar 2016 13:56:53 -0500 Subject: [PATCH 2/3] initial version, no auth, needs testing --- haas/api.py | 14 +++++++------- haas/model.py | 19 ++++++++++++------- tests/unit/api.py | 8 ++++---- tests/unit/model.py | 4 ++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/haas/api.py b/haas/api.py index 03193b8a..0f83ae7d 100644 --- a/haas/api.py +++ b/haas/api.py @@ -294,7 +294,7 @@ def _have_attachment(nic, query): if nic.current_action: raise BlockedError("A networking operation is already active on the nic.") - if (network.access is not None) and (network.access is not project): + if (network.access) and (project not in network.access): raise ProjectMismatchError("Project does not have access to given network.") if _have_attachment(nic, model.NetworkAttachment.network == network): @@ -490,7 +490,7 @@ def headnode_connect_network(headnode, hnic, network): project = headnode.project - if (network.access is not None) and (network.access is not project): + if (network.access) and (project not in network.access): raise ProjectMismatchError("Project does not have access to given network.") hnic.network = network @@ -547,14 +547,14 @@ def network_create(network, creator, access, net_id): if net_id != "": raise BadArgumentError("Project-created networks must use network ID allocation") creator = _must_find(model.Project, creator) - access = _must_find(model.Project, access) + access = [_must_find(model.Project, access)] else: # Administrator-owned network creator = None if access == "": - access = None + access = [] else: - access = _must_find(model.Project, access) + access = [_must_find(model.Project, access)] # Allocate net_id, if requested if net_id == "": @@ -614,8 +614,8 @@ def show_network(network): else: result['creator'] = network.creator.label - if network.access is not None: - result['access'] = network.access.label + if network.access: + result['access'] = [p.label for p in network.access] return json.dumps(result) diff --git a/haas/model.py b/haas/model.py index 353c661c..d5341980 100644 --- a/haas/model.py +++ b/haas/model.py @@ -36,6 +36,11 @@ Column('user_id', ForeignKey('user.id')), Column('project_id', ForeignKey('project.id'))) +# A joining table for networks and projects, which have a many to many relationship: +network_projects = Table('network_projects', Base.metadata, + Column('project_id', ForeignKey('project.id')), + Column('network_id', ForeignKey('network.id'))) + def init_db(create=False, uri=None): """Start up the DB connection. @@ -257,13 +262,6 @@ class Network(Model): creator = relationship("Project", backref=backref('networks_created'), foreign_keys=[creator_id]) - # The project that has access to the network, or None if the network is - # public. This field determines who can connect a node or headnode to a - # network. - access_id = Column(ForeignKey('project.id')) - access = relationship("Project", - backref=backref('networks_access'), - foreign_keys=[access_id]) # True if network_id was allocated by the driver; False if it was # assigned by an administrator. allocated = Column(Boolean) @@ -271,6 +269,13 @@ class Network(Model): # An identifier meaningful to the networking driver: network_id = Column(String, nullable=False) + # The project that has access to the network, or None if the network is + # public. This field determines who can connect a node or headnode to a + # network. + access = relationship("Project", + backref=backref('networks_access'), + secondary='network_projects') + def __init__(self, creator, access, allocated, network_id, label): """Create a network. diff --git a/tests/unit/api.py b/tests/unit/api.py index ef7f5823..f20ebdae 100644 --- a/tests/unit/api.py +++ b/tests/unit/api.py @@ -1416,7 +1416,7 @@ def test_show_network_simple(self, db): assert result == { 'name': 'spiderwebs', 'creator': 'anvil-nextgen', - 'access': 'anvil-nextgen', + 'access': ['anvil-nextgen'], "channels": ["null"] } @@ -1444,7 +1444,7 @@ def test_show_network_provider(self, db): assert result == { 'name': 'spiderwebs', 'creator': 'admin', - 'access': 'anvil-nextgen', + 'access': ['anvil-nextgen'], 'channels': ['null'], } @@ -1467,7 +1467,7 @@ def test_project_network(self, db): project = api._must_find(model.Project, 'anvil-nextgen') network = api._must_find(model.Network, 'hammernet') assert network.creator is project - assert network.access is project + assert project in network.access assert network.allocated is True def test_project_network_imported_fails(self, db): @@ -1495,7 +1495,7 @@ def test_admin_network(self, db): api.network_create(network, 'admin', project_api, net_id) network = api._must_find(model.Network, network) assert network.creator is None - assert network.access is project_db + assert (project_db is None and not network.access) or project_db in network.access assert network.allocated is allocated network = api._must_find(model.Network, 'hammernet' + project_api + '35') assert network.network_id == '35' diff --git a/tests/unit/model.py b/tests/unit/model.py index 7f9b2d21..24cc4253 100644 --- a/tests/unit/model.py +++ b/tests/unit/model.py @@ -109,7 +109,7 @@ class TestNetwork(ModelTest): def sample_obj(self): pj = Project('anvil-nextgen') - return Network(pj, pj, True, '102', 'hammernet') + return Network(pj, [pj], True, '102', 'hammernet') class TestNetworkingAction(ModelTest): @@ -117,7 +117,7 @@ def sample_obj(self): nic = Nic(Node('node-99', 'ipmihost', 'root', 'tapeworm'), 'ipmi', '00:11:22:33:44:55') project = Project('anvil-nextgen') - network = Network(project, project, True, '102', 'hammernet') + network = Network(project, [project], True, '102', 'hammernet') return NetworkingAction(nic=nic, new_network=network, channel='null') From c0cb2c153cae713a44a57443791316472b6d0dc3 Mon Sep 17 00:00:00 2001 From: Kyle Hogan Date: Sun, 13 Mar 2016 19:44:57 -0400 Subject: [PATCH 3/3] can grant and revoke network access for a project --- haas/api.py | 39 +++++++++++++++++++++++++++++++++++++++ haas/cli.py | 14 ++++++++++++++ tests/unit/api.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/haas/api.py b/haas/api.py index 0f83ae7d..62b5e0e2 100644 --- a/haas/api.py +++ b/haas/api.py @@ -179,6 +179,45 @@ def project_remove_user(project, user): local.db.commit() +@rest_call('POST', '/project//add_network') +def project_add_network(project, network): + """Add a network to a project. + + If the project or network does not exist, a NotFoundError will be raised. + """ + network = _must_find(model.Network, network) + project = _must_find(model.Project, project) + if project in network.access: + raise DuplicateError('Network %s is already in project %s'% + (network.label, project.label)) + + network.access.append(project) + local.db.commit() + + + +@rest_call('POST', '/project//remove_network') +def project_remove_network(project, network): + """Remove a network from a project. + + If the project or network does not exist, a NotFoundError will be raised. + If the project is the creator of the netwrok a BlockedError will be raised. + """ + network = _must_find(model.Network, network) + project = _must_find(model.Project, project) + if project not in network.access: + raise NotFoundError("Network %s is not in project %s"% + (network.label, project.label)) + + if project is network.creator: + raise BlockedError("Project %s is creator of network %s and cannot be removed"% + (project.label, network.label)) + + network.access.remove(project) + local.db.commit() + + + # Node Code # ############# diff --git a/haas/cli.py b/haas/cli.py index 78de4e94..4c9cb62b 100644 --- a/haas/cli.py +++ b/haas/cli.py @@ -186,6 +186,20 @@ def project_remove_user(project, user): url = object_url('project', project, 'remove_user') do_post(url, data={'user': user}) + +@cmd +def project_add_network(project, network): + """Add to """ + url = object_url('project', project, 'add_network') + do_post(url, data={'network': network}) + + +@cmd +def project_remove_network(project, network): + """Remove from """ + url = object_url('project', project, 'remove_network') + do_post(url, data={'network': network}) + @cmd def project_create(project): """Create a """ diff --git a/tests/unit/api.py b/tests/unit/api.py index f20ebdae..e9f1946e 100644 --- a/tests/unit/api.py +++ b/tests/unit/api.py @@ -187,6 +187,36 @@ def test_bad_project_remove_user(self, db): api.project_remove_user('acme-corp', 'alice') +class TestProjectAddDeleteNetwork: + """Tests for adding and deleting a network from a project""" + + def test_project_add_network(self, db): + api.project_create('acme-corp') + api.project_create('anvil-nextgen') + network_create_simple('hammernet', 'acme-corp') + api.project_add_network('anvil-nextgen', 'hammernet') + network = api._must_find(model.Network, 'hammernet') + project = api._must_find(model.Project, 'anvil-nextgen') + assert project in network.access + assert network in project.networks_access + + def test_project_remove_network(self, db): + api.project_create('acme-corp') + api.project_create('anvil-nextgen') + network_create_simple('hammernet', 'acme-corp') + api.project_add_network('anvil-nextgen', 'hammernet') + api.project_remove_network('anvil-nextgen', 'hammernet') + network = api._must_find(model.Network, 'hammernet') + project = api._must_find(model.Project, 'anvil-nextgen') + assert project not in network.access + assert network not in project.networks_access + + def test_project_remove_network_creator(self, db): + api.project_create('acme-corp') + network_create_simple('hammernet', 'acme-corp') + with pytest.raises(api.BlockedError): + api.project_remove_network('acme-corp', 'hammernet') + class TestNetworking: def test_networking_involved(self, db):