Browse Source

Merge "Introduce Neutron DHCP agent commands to OSC"

tags/3.10.0
Jenkins 2 years ago
parent
commit
3e621c9a9c

+ 55
- 0
doc/source/command-objects/network-agent.rst View File

@@ -10,6 +10,31 @@ agent is "True".
10 10
 
11 11
 Network v2
12 12
 
13
+network agent add network
14
+-------------------------
15
+
16
+Add network to an agent
17
+
18
+.. program:: network agent add network
19
+.. code:: bash
20
+
21
+    openstack network agent add network
22
+        [--dhcp]
23
+        <agent-id>
24
+        <network>
25
+
26
+.. describe:: --dhcp
27
+
28
+    Add a network to DHCP agent.
29
+
30
+.. describe:: <agent-id>
31
+
32
+    Agent to which a network is added. (ID only)
33
+
34
+.. describe:: <network>
35
+
36
+    Network to be added to an agent. (ID or name)
37
+
13 38
 network agent delete
14 39
 --------------------
15 40
 
@@ -37,6 +62,7 @@ List network agents
37 62
     openstack network agent list
38 63
         [--agent-type <agent-type>]
39 64
         [--host <host>]
65
+        [--network <network>]
40 66
 
41 67
 .. option:: --agent-type <agent-type>
42 68
 
@@ -49,6 +75,10 @@ List network agents
49 75
 
50 76
     List only agents running on the specified host
51 77
 
78
+.. option:: --network <network>
79
+
80
+    List agents hosting a network. (ID or name)
81
+
52 82
 network agent set
53 83
 -----------------
54 84
 
@@ -94,3 +124,28 @@ Display network agent details
94 124
 .. describe:: <network-agent>
95 125
 
96 126
     Network agent to display (ID only)
127
+
128
+network agent remove network
129
+----------------------------
130
+
131
+Remove network from an agent
132
+
133
+.. program:: network agent remove network
134
+.. code:: bash
135
+
136
+    openstack network agent remove network
137
+        [--dhcp]
138
+        <agent-id>
139
+        <network>
140
+
141
+.. describe:: --dhcp
142
+
143
+    Remove network from DHCP agent.
144
+
145
+.. describe:: <agent-id>
146
+
147
+    Agent to which a network is removed. (ID only)
148
+
149
+.. describe:: <network>
150
+
151
+    Network to be removed from an agent. (ID or name)

+ 5
- 0
doc/source/command-objects/network.rst View File

@@ -203,6 +203,7 @@ List networks
203 203
         [--provider-network-type <provider-network-type>]
204 204
         [--provider-physical-network <provider-physical-network>]
205 205
         [--provider-segment <provider-segment>]
206
+        [--agent <agent-id>]
206 207
 
207 208
 .. option:: --external
208 209
 
@@ -290,6 +291,10 @@ List networks
290 291
 
291 292
     *Network version 2 only*
292 293
 
294
+.. option:: --agent <agent-id>
295
+
296
+    List networks hosted by agent (ID only)
297
+
293 298
 network set
294 299
 -----------
295 300
 

+ 33
- 11
openstackclient/network/v2/network.py View File

@@ -60,12 +60,10 @@ def _get_network_columns(item):
60 60
 
61 61
 
62 62
 def _get_columns(item):
63
-    columns = list(item.keys())
64
-    if 'tenant_id' in columns:
65
-        columns.remove('tenant_id')
66
-    if 'project_id' not in columns:
67
-        columns.append('project_id')
68
-    return tuple(sorted(columns))
63
+    column_map = {
64
+        'tenant_id': 'project_id',
65
+    }
66
+    return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map)
69 67
 
70 68
 
71 69
 def _get_attrs(client_manager, parsed_args):
@@ -305,9 +303,9 @@ class CreateNetwork(common.NetworkAndComputeShowOne):
305 303
     def take_action_compute(self, client, parsed_args):
306 304
         attrs = _get_attrs_compute(self.app.client_manager, parsed_args)
307 305
         obj = client.networks.create(**attrs)
308
-        columns = _get_columns(obj._info)
306
+        display_columns, columns = _get_columns(obj._info)
309 307
         data = utils.get_dict_properties(obj._info, columns)
310
-        return (columns, data)
308
+        return (display_columns, data)
311 309
 
312 310
 
313 311
 class DeleteNetwork(common.NetworkAndComputeDelete):
@@ -420,7 +418,11 @@ class ListNetwork(common.NetworkAndComputeLister):
420 418
             help=_("List networks according to VLAN ID for VLAN networks "
421 419
                    "or Tunnel ID for GENEVE/GRE/VXLAN networks")
422 420
         )
423
-
421
+        parser.add_argument(
422
+            '--agent',
423
+            metavar='<agent-id>',
424
+            dest='agent_id',
425
+            help=_('List networks hosted by agent (ID only)'))
424 426
         return parser
425 427
 
426 428
     def take_action_network(self, client, parsed_args):
@@ -450,6 +452,26 @@ class ListNetwork(common.NetworkAndComputeLister):
450 452
                 'Router Type',
451 453
                 'Availability Zones',
452 454
             )
455
+        elif parsed_args.agent_id:
456
+            columns = (
457
+                'id',
458
+                'name',
459
+                'subnet_ids'
460
+            )
461
+            column_headers = (
462
+                'ID',
463
+                'Name',
464
+                'Subnets',
465
+            )
466
+            client = self.app.client_manager.network
467
+            dhcp_agent = client.get_agent(parsed_args.agent_id)
468
+            data = client.dhcp_agent_hosting_networks(dhcp_agent)
469
+
470
+            return (column_headers,
471
+                    (utils.get_item_properties(
472
+                        s, columns,
473
+                        formatters=_formatters,
474
+                    ) for s in data))
453 475
         else:
454 476
             columns = (
455 477
                 'id',
@@ -665,6 +687,6 @@ class ShowNetwork(common.NetworkAndComputeShowOne):
665 687
             client.networks,
666 688
             parsed_args.network,
667 689
         )
668
-        columns = _get_columns(obj._info)
690
+        display_columns, columns = _get_columns(obj._info)
669 691
         data = utils.get_dict_properties(obj._info, columns)
670
-        return (columns, data)
692
+        return (display_columns, data)

+ 105
- 11
openstackclient/network/v2/network_agent.py View File

@@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__)
29 29
 def _format_admin_state(state):
30 30
     return 'UP' if state else 'DOWN'
31 31
 
32
-
33 32
 _formatters = {
34 33
     'admin_state_up': _format_admin_state,
35 34
     'is_admin_state_up': _format_admin_state,
@@ -45,6 +44,40 @@ def _get_network_columns(item):
45 44
     return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map)
46 45
 
47 46
 
47
+class AddNetworkToAgent(command.Command):
48
+    _description = _("Add network to an agent")
49
+
50
+    def get_parser(self, prog_name):
51
+        parser = super(AddNetworkToAgent, self).get_parser(prog_name)
52
+        parser.add_argument(
53
+            '--dhcp',
54
+            action='store_true',
55
+            help=_('Add network to a DHCP agent'))
56
+        parser.add_argument(
57
+            'agent_id',
58
+            metavar='<agent-id>',
59
+            help=_('Agent to which a network is added. (ID only)'))
60
+        parser.add_argument(
61
+            'network',
62
+            metavar='<network>',
63
+            help=_('Network to be added to an agent.  (ID or name)'))
64
+
65
+        return parser
66
+
67
+    def take_action(self, parsed_args):
68
+        client = self.app.client_manager.network
69
+        agent = client.get_agent(parsed_args.agent_id)
70
+        if parsed_args.dhcp:
71
+            network = client.find_network(
72
+                parsed_args.network, ignore_missing=False)
73
+            try:
74
+                client.add_dhcp_agent_to_network(agent, network)
75
+            except Exception:
76
+                msg = 'Failed to add {} to {}'.format(
77
+                    network.name, agent.agent_type)
78
+                exceptions.CommandError(msg)
79
+
80
+
48 81
 class DeleteNetworkAgent(command.Command):
49 82
     _description = _("Delete network agent(s)")
50 83
 
@@ -102,6 +135,11 @@ class ListNetworkAgent(command.Lister):
102 135
             metavar='<host>',
103 136
             help=_("List only agents running on the specified host")
104 137
         )
138
+        parser.add_argument(
139
+            '--network',
140
+            metavar='<network>',
141
+            help=_('List agents hosting a network (name or ID)')
142
+        )
105 143
         return parser
106 144
 
107 145
     def take_action(self, parsed_args):
@@ -140,16 +178,72 @@ class ListNetworkAgent(command.Lister):
140 178
         }
141 179
 
142 180
         filters = {}
143
-        if parsed_args.agent_type is not None:
144
-            filters['agent_type'] = key_value[parsed_args.agent_type]
145
-        if parsed_args.host is not None:
146
-            filters['host'] = parsed_args.host
147
-
148
-        data = client.agents(**filters)
149
-        return (column_headers,
150
-                (utils.get_item_properties(
151
-                    s, columns, formatters=_formatters,
152
-                ) for s in data))
181
+        if parsed_args.network is not None:
182
+            columns = (
183
+                'id',
184
+                'host',
185
+                'is_admin_state_up',
186
+                'is_alive',
187
+            )
188
+            column_headers = (
189
+                'ID',
190
+                'Host',
191
+                'Admin State Up',
192
+                'Alive',
193
+            )
194
+            network = client.find_network(
195
+                parsed_args.network, ignore_missing=False)
196
+            data = client.network_hosting_dhcp_agents(network)
197
+
198
+            return (column_headers,
199
+                    (utils.get_item_properties(
200
+                        s, columns,
201
+                        formatters=_formatters,
202
+                    ) for s in data))
203
+        else:
204
+            if parsed_args.agent_type is not None:
205
+                filters['agent_type'] = key_value[parsed_args.agent_type]
206
+            if parsed_args.host is not None:
207
+                filters['host'] = parsed_args.host
208
+
209
+            data = client.agents(**filters)
210
+            return (column_headers,
211
+                    (utils.get_item_properties(
212
+                        s, columns, formatters=_formatters,
213
+                    ) for s in data))
214
+
215
+
216
+class RemoveNetworkFromAgent(command.Command):
217
+    _description = _("Remove network from an agent.")
218
+
219
+    def get_parser(self, prog_name):
220
+        parser = super(RemoveNetworkFromAgent, self).get_parser(prog_name)
221
+        parser.add_argument(
222
+            '--dhcp',
223
+            action='store_true',
224
+            help=_('Remove network from DHCP agent'))
225
+        parser.add_argument(
226
+            'agent_id',
227
+            metavar='<agent-id>',
228
+            help=_('Agent to which a network is removed. (ID only)'))
229
+        parser.add_argument(
230
+            'network',
231
+            metavar='<network>',
232
+            help=_('Network to be removed from an agent. (ID or name)'))
233
+        return parser
234
+
235
+    def take_action(self, parsed_args):
236
+        client = self.app.client_manager.network
237
+        agent = client.get_agent(parsed_args.agent_id)
238
+        if parsed_args.dhcp:
239
+            network = client.find_network(
240
+                parsed_args.network, ignore_missing=False)
241
+            try:
242
+                client.remove_dhcp_agent_from_network(agent, network)
243
+            except Exception:
244
+                msg = 'Failed to remove {} to {}'.format(
245
+                    network.name, agent.agent_type)
246
+                exceptions.CommandError(msg)
153 247
 
154 248
 
155 249
 # TODO(huanxuan): Use the SDK resource mapped attribute names once the

+ 43
- 0
openstackclient/tests/functional/network/v2/test_network.py View File

@@ -238,6 +238,49 @@ class NetworkTests(base.TestCase):
238 238
         self.assertIn(name1, col_name)
239 239
         self.assertNotIn(name2, col_name)
240 240
 
241
+    def test_network_dhcp_agent(self):
242
+        name1 = uuid.uuid4().hex
243
+        cmd_output1 = json.loads(self.openstack(
244
+            'network create -f json ' +
245
+            '--description aaaa ' +
246
+            name1
247
+        ))
248
+
249
+        self.addCleanup(self.openstack, 'network delete ' + name1)
250
+
251
+        # Get network ID
252
+        network_id = cmd_output1['id']
253
+
254
+        # Get DHCP Agent ID
255
+        cmd_output2 = json.loads(self.openstack(
256
+            'network agent list -f json --agent-type dhcp'
257
+        ))
258
+        agent_id = cmd_output2[0]['ID']
259
+
260
+        # Add Agent to Network
261
+        self.openstack(
262
+            'network agent add network --dhcp '
263
+            + agent_id + ' ' + network_id
264
+        )
265
+
266
+        # Test network list --agent
267
+        cmd_output3 = json.loads(self.openstack(
268
+            'network list -f json --agent ' + agent_id
269
+        ))
270
+
271
+        # Cleanup
272
+        # Remove Agent from Network
273
+        self.openstack(
274
+            'network agent remove network --dhcp '
275
+            + agent_id + ' ' + network_id
276
+        )
277
+
278
+        # Assert
279
+        col_name = [x["ID"] for x in cmd_output3]
280
+        self.assertIn(
281
+            network_id, col_name
282
+        )
283
+
241 284
     def test_network_set(self):
242 285
         """Tests create options, set, show, delete"""
243 286
         name = uuid.uuid4().hex

+ 52
- 0
openstackclient/tests/functional/network/v2/test_network_agent.py View File

@@ -10,6 +10,9 @@
10 10
 #    License for the specific language governing permissions and limitations
11 11
 #    under the License.
12 12
 
13
+import json
14
+import uuid
15
+
13 16
 from openstackclient.tests.functional import base
14 17
 
15 18
 
@@ -39,3 +42,52 @@ class NetworkAgentTests(base.TestCase):
39 42
         self.openstack('network agent set --enable ' + self.IDs[0])
40 43
         raw_output = self.openstack('network agent show ' + self.IDs[0] + opts)
41 44
         self.assertEqual("UP\n", raw_output)
45
+
46
+
47
+class NetworkAgentListTests(base.TestCase):
48
+    """Functional test for network agent list --network. """
49
+
50
+    def test_network_dhcp_agent_list(self):
51
+        """Test network agent list"""
52
+
53
+        name1 = uuid.uuid4().hex
54
+        cmd_output1 = json.loads(self.openstack(
55
+            'network create -f json ' +
56
+            '--description aaaa ' +
57
+            name1
58
+        ))
59
+
60
+        self.addCleanup(self.openstack, 'network delete ' + name1)
61
+
62
+        # Get network ID
63
+        network_id = cmd_output1['id']
64
+
65
+        # Get DHCP Agent ID
66
+        cmd_output2 = json.loads(self.openstack(
67
+            'network agent list -f json --agent-type dhcp'
68
+        ))
69
+        agent_id = cmd_output2[0]['ID']
70
+
71
+        # Add Agent to Network
72
+        self.openstack(
73
+            'network agent add network --dhcp '
74
+            + agent_id + ' ' + network_id
75
+        )
76
+
77
+        # Test network agent list --network
78
+        cmd_output3 = json.loads(self.openstack(
79
+            'network agent list -f json --network ' + network_id
80
+        ))
81
+
82
+        # Cleanup
83
+        # Remove Agent from Network
84
+        self.openstack(
85
+            'network agent remove network --dhcp '
86
+            + agent_id + ' ' + network_id
87
+        )
88
+
89
+        # Assert
90
+        col_name = [x["ID"] for x in cmd_output3]
91
+        self.assertIn(
92
+            agent_id, col_name
93
+        )

+ 26
- 0
openstackclient/tests/unit/network/v2/test_network.py View File

@@ -491,6 +491,13 @@ class TestListNetwork(TestNetwork):
491 491
 
492 492
         self.network.networks = mock.Mock(return_value=self._network)
493 493
 
494
+        self._agent = \
495
+            network_fakes.FakeNetworkAgent.create_one_network_agent()
496
+        self.network.get_agent = mock.Mock(return_value=self._agent)
497
+
498
+        self.network.dhcp_agent_hosting_networks = mock.Mock(
499
+            return_value=self._network)
500
+
494 501
     def test_network_list_no_options(self):
495 502
         arglist = []
496 503
         verifylist = [
@@ -765,6 +772,25 @@ class TestListNetwork(TestNetwork):
765 772
         self.assertEqual(self.columns, columns)
766 773
         self.assertEqual(self.data, list(data))
767 774
 
775
+    def test_network_list_dhcp_agent(self):
776
+        arglist = [
777
+            '--agent', self._agent.id
778
+        ]
779
+        verifylist = [
780
+            ('agent_id', self._agent.id),
781
+        ]
782
+
783
+        attrs = {self._agent, }
784
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
785
+
786
+        columns, data = self.cmd.take_action(parsed_args)
787
+
788
+        self.network.dhcp_agent_hosting_networks.assert_called_once_with(
789
+            *attrs)
790
+
791
+        self.assertEqual(self.columns, columns)
792
+        self.assertEqual(list(data), list(self.data))
793
+
768 794
 
769 795
 class TestSetNetwork(TestNetwork):
770 796
 

+ 128
- 1
openstackclient/tests/unit/network/v2/test_network_agent.py View File

@@ -31,6 +31,48 @@ class TestNetworkAgent(network_fakes.TestNetworkV2):
31 31
         self.network = self.app.client_manager.network
32 32
 
33 33
 
34
+class TestAddNetworkToAgent(TestNetworkAgent):
35
+
36
+    net = network_fakes.FakeNetwork.create_one_network()
37
+    agent = network_fakes.FakeNetworkAgent.create_one_network_agent()
38
+
39
+    def setUp(self):
40
+        super(TestAddNetworkToAgent, self).setUp()
41
+
42
+        self.network.get_agent = mock.Mock(return_value=self.agent)
43
+        self.network.find_network = mock.Mock(return_value=self.net)
44
+        self.network.name = self.network.find_network.name
45
+        self.network.add_dhcp_agent_to_network = mock.Mock()
46
+        self.cmd = network_agent.AddNetworkToAgent(
47
+            self.app, self.namespace)
48
+
49
+    def test_show_no_options(self):
50
+        arglist = []
51
+        verifylist = []
52
+
53
+        # Missing required args should bail here
54
+        self.assertRaises(tests_utils.ParserException, self.check_parser,
55
+                          self.cmd, arglist, verifylist)
56
+
57
+    def test_add_network_to_dhcp_agent(self):
58
+        arglist = [
59
+            '--dhcp',
60
+            self.agent.id,
61
+            self.net.id
62
+        ]
63
+        verifylist = [
64
+            ('dhcp', True),
65
+            ('agent_id', self.agent.id),
66
+            ('network', self.net.id),
67
+        ]
68
+
69
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
70
+        self.cmd.take_action(parsed_args)
71
+
72
+        self.network.add_dhcp_agent_to_network.assert_called_once_with(
73
+            self.agent, self.net)
74
+
75
+
34 76
 class TestDeleteNetworkAgent(TestNetworkAgent):
35 77
 
36 78
     network_agents = (
@@ -66,7 +108,6 @@ class TestDeleteNetworkAgent(TestNetworkAgent):
66 108
 
67 109
     def test_multi_network_agents_delete(self):
68 110
         arglist = []
69
-        verifylist = []
70 111
 
71 112
         for n in self.network_agents:
72 113
             arglist.append(n.id)
@@ -141,11 +182,37 @@ class TestListNetworkAgent(TestNetworkAgent):
141 182
             agent.binary,
142 183
         ))
143 184
 
185
+    network_agent_columns = (
186
+        'ID',
187
+        'Host',
188
+        'Admin State Up',
189
+        'Alive',
190
+    )
191
+
192
+    network_agent_data = []
193
+
194
+    for agent in network_agents:
195
+        network_agent_data.append((
196
+            agent.id,
197
+            agent.host,
198
+            network_agent._format_admin_state(agent.admin_state_up),
199
+            agent.alive,
200
+        ))
201
+
144 202
     def setUp(self):
145 203
         super(TestListNetworkAgent, self).setUp()
146 204
         self.network.agents = mock.Mock(
147 205
             return_value=self.network_agents)
148 206
 
207
+        _testagent = \
208
+            network_fakes.FakeNetworkAgent.create_one_network_agent()
209
+        self.network.get_agent = mock.Mock(return_value=_testagent)
210
+
211
+        self._testnetwork = network_fakes.FakeNetwork.create_one_network()
212
+        self.network.find_network = mock.Mock(return_value=self._testnetwork)
213
+        self.network.network_hosting_dhcp_agents = mock.Mock(
214
+            return_value=self.network_agents)
215
+
149 216
         # Get the command object to test
150 217
         self.cmd = network_agent.ListNetworkAgent(self.app, self.namespace)
151 218
 
@@ -194,6 +261,66 @@ class TestListNetworkAgent(TestNetworkAgent):
194 261
         self.assertEqual(self.columns, columns)
195 262
         self.assertEqual(self.data, list(data))
196 263
 
264
+    def test_network_agents_list_networks(self):
265
+        arglist = [
266
+            '--network', self._testnetwork.id,
267
+        ]
268
+        verifylist = [
269
+            ('network', self._testnetwork.id),
270
+        ]
271
+
272
+        attrs = {self._testnetwork, }
273
+
274
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
275
+        columns, data = self.cmd.take_action(parsed_args)
276
+
277
+        self.network.network_hosting_dhcp_agents.assert_called_once_with(
278
+            *attrs)
279
+        self.assertEqual(self.network_agent_columns, columns)
280
+        self.assertEqual(list(self.network_agent_data), list(data))
281
+
282
+
283
+class TestRemoveNetworkFromAgent(TestNetworkAgent):
284
+
285
+    net = network_fakes.FakeNetwork.create_one_network()
286
+    agent = network_fakes.FakeNetworkAgent.create_one_network_agent()
287
+
288
+    def setUp(self):
289
+        super(TestRemoveNetworkFromAgent, self).setUp()
290
+
291
+        self.network.get_agent = mock.Mock(return_value=self.agent)
292
+        self.network.find_network = mock.Mock(return_value=self.net)
293
+        self.network.name = self.network.find_network.name
294
+        self.network.remove_dhcp_agent_from_network = mock.Mock()
295
+        self.cmd = network_agent.RemoveNetworkFromAgent(
296
+            self.app, self.namespace)
297
+
298
+    def test_show_no_options(self):
299
+        arglist = []
300
+        verifylist = []
301
+
302
+        # Missing required args should bail here
303
+        self.assertRaises(tests_utils.ParserException, self.check_parser,
304
+                          self.cmd, arglist, verifylist)
305
+
306
+    def test_network_from_dhcp_agent(self):
307
+        arglist = [
308
+            '--dhcp',
309
+            self.agent.id,
310
+            self.net.id
311
+        ]
312
+        verifylist = [
313
+            ('dhcp', True),
314
+            ('agent_id', self.agent.id),
315
+            ('network', self.net.id),
316
+        ]
317
+
318
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
319
+        self.cmd.take_action(parsed_args)
320
+
321
+        self.network.remove_dhcp_agent_from_network.assert_called_once_with(
322
+            self.agent, self.net)
323
+
197 324
 
198 325
 # TODO(huanxuan): Also update by the new attribute name
199 326
 # "is_admin_state_up" after sdk 0.9.12

+ 7
- 0
releasenotes/notes/bp-network-dhcp-adv-commands-e61bf8757f46dc93.yaml View File

@@ -0,0 +1,7 @@
1
+---
2
+features:
3
+  - |
4
+    Add network dhcp-agent related commands ``network agent add network``,
5
+    ``network agent remove network``, ``network agent list --network`` and
6
+    ``network list --agent`` for adding/removing network to dhcp agent.
7
+    [Blueprint :oscbp:`network-dhcp-adv-commands`]

+ 2
- 0
setup.cfg View File

@@ -357,8 +357,10 @@ openstack.network.v2 =
357 357
 
358 358
     ip_floating_pool_list = openstackclient.network.v2.floating_ip_pool:ListIPFloatingPool
359 359
 
360
+    network_agent_add_network = openstackclient.network.v2.network_agent:AddNetworkToAgent
360 361
     network_agent_delete = openstackclient.network.v2.network_agent:DeleteNetworkAgent
361 362
     network_agent_list = openstackclient.network.v2.network_agent:ListNetworkAgent
363
+    network_agent_remove_network = openstackclient.network.v2.network_agent:RemoveNetworkFromAgent
362 364
     network_agent_set = openstackclient.network.v2.network_agent:SetNetworkAgent
363 365
     network_agent_show = openstackclient.network.v2.network_agent:ShowNetworkAgent
364 366
 

Loading…
Cancel
Save