blob: d10dd2f8fd470961b0c7fd5f38bd228333d6734c [file] [log] [blame]
Václav Kubernátc31bd602019-03-07 11:44:48 +01001/*
2 * Copyright (C) 2019 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Václav Kubernát <kubernat@cesnet.cz>
5 * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
6 *
7*/
8
9#include <libyang/Tree_Data.hpp>
10#include <mutex>
11extern "C" {
12#include <nc_client.h>
13}
14#include <sstream>
15#include <tuple>
16#include "netconf-client.h"
17
18namespace libnetconf {
19
20namespace impl {
21
22/** @short Initialization of the libnetconf2 library client
23
24Just a safe wrapper over nc_client_init and nc_client_destroy, really.
25*/
26class ClientInit {
27 ClientInit()
28 {
29 nc_client_init();
30 nc_verbosity(NC_VERB_DEBUG);
31 }
32
33 ~ClientInit()
34 {
35 nc_client_destroy();
36 }
37
38public:
39 static ClientInit& instance()
40 {
41 static ClientInit lib;
42 return lib;
43 }
44
45 ClientInit(const ClientInit&) = delete;
46 ClientInit(ClientInit&&) = delete;
47 ClientInit& operator=(const ClientInit&) = delete;
48 ClientInit& operator=(ClientInit&&) = delete;
49};
50
51static std::mutex clientOptions;
52
53inline void custom_free_nc_reply_data(nc_reply_data* reply)
54{
55 nc_reply_free(reinterpret_cast<nc_reply*>(reply));
56}
57inline void custom_free_nc_reply_error(nc_reply_error* reply)
58{
59 nc_reply_free(reinterpret_cast<nc_reply*>(reply));
60}
61
62template <auto fn>
63using deleter_from_fn = std::integral_constant<decltype(fn), fn>;
64
65template <typename T>
66struct deleter_type_for {
67 using func_type = void (*)(T*);
68};
69
70template <typename T>
71struct deleter_for {
72};
73
74template <>
75struct deleter_for<struct nc_rpc> {
76 static constexpr void (*free)(struct nc_rpc*) = deleter_from_fn<nc_rpc_free>();
77};
78template <>
79struct deleter_for<struct nc_reply> {
80 static constexpr void (*free)(struct nc_reply*) = deleter_from_fn<nc_reply_free>();
81};
82template <>
83struct deleter_for<struct nc_reply_data> {
84 static constexpr void (*free)(struct nc_reply_data*) = deleter_from_fn<custom_free_nc_reply_data>();
85};
86template <>
87struct deleter_for<struct nc_reply_error> {
88 static constexpr void (*free)(struct nc_reply_error*) = deleter_from_fn<custom_free_nc_reply_error>();
89};
90
91template <typename T>
92using unique_ptr_for = std::unique_ptr<T, decltype(deleter_for<T>::free)>;
93
94template <typename T>
95auto guarded(T* ptr)
96{
97 return unique_ptr_for<T>(ptr, deleter_for<T>::free);
98}
99
100unique_ptr_for<struct nc_reply> do_rpc(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
101{
102 uint64_t msgid;
103 NC_MSG_TYPE msgtype;
104
105 msgtype = nc_send_rpc(session->session_internal(), rpc.get(), 1000, &msgid);
106 if (msgtype == NC_MSG_ERROR) {
107 throw std::runtime_error{"Failed to send RPC"};
108 }
109 if (msgtype == NC_MSG_WOULDBLOCK) {
110 throw std::runtime_error{"Timeout sending an RPC"};
111 }
112
113 struct nc_reply* raw_reply;
114 while (true) {
115 msgtype = nc_recv_reply(session->session_internal(), rpc.get(), msgid, 20000, LYD_OPT_DESTRUCT | LYD_OPT_NOSIBLINGS, &raw_reply);
116 auto reply = guarded(raw_reply);
117 raw_reply = nullptr;
118
119 switch (msgtype) {
120 case NC_MSG_ERROR:
121 throw std::runtime_error{"Failed to receive an RPC reply"};
122 case NC_MSG_WOULDBLOCK:
123 throw std::runtime_error{"Timed out waiting for RPC reply"};
124 case NC_MSG_REPLY_ERR_MSGID:
125 throw std::runtime_error{"Received a wrong reply -- msgid mismatch"};
126 case NC_MSG_NOTIF:
127 continue;
128 default:
129 return reply;
130 }
131 }
132 __builtin_unreachable();
133}
134
135client::ReportedError make_error(unique_ptr_for<struct nc_reply>&& reply)
136{
137 if (reply->type != NC_RPL_ERROR) {
138 throw std::logic_error{"Cannot extract an error from a non-error server reply"};
139 }
140
141 auto errorReply = guarded(reinterpret_cast<struct nc_reply_error*>(reply.release()));
142
143 // TODO: capture the error details, not just that human-readable string
144 std::ostringstream ss;
145 ss << "Server error:" << std::endl;
146 for (uint32_t i = 0; i < errorReply->count; ++i) {
147 const auto e = errorReply->err[i];
148 ss << " #" << i << ": " << e.message;
149 if (e.path) {
150 ss << " (XPath " << e.path << ")";
151 }
152 ss << std::endl;
153 }
154 return client::ReportedError{ss.str()};
155}
156
157unique_ptr_for<struct nc_reply_data> do_rpc_data(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
158{
159 auto x = do_rpc(session, std::move(rpc));
160
161 switch (x->type) {
162 case NC_RPL_DATA:
163 return guarded(reinterpret_cast<struct nc_reply_data*>(x.release()));
164 case NC_RPL_OK:
165 throw std::runtime_error{"Received OK instead of a data reply"};
166 case NC_RPL_ERROR:
167 throw make_error(std::move(x));
168 default:
169 throw std::runtime_error{"Unhandled reply type"};
170 }
171}
172
173void do_rpc_ok(client::Session* session, unique_ptr_for<struct nc_rpc>&& rpc)
174{
175 auto x = do_rpc(session, std::move(rpc));
176
177 switch (x->type) {
178 case NC_RPL_DATA:
179 throw std::runtime_error{"Unexpected DATA reply"};
180 case NC_RPL_OK:
181 return;
182 case NC_RPL_ERROR:
183 throw make_error(std::move(x));
184 default:
185 throw std::runtime_error{"Unhandled reply type"};
186 }
187}
188}
189
190namespace client {
191
192struct nc_session* Session::session_internal()
193{
194 return m_session;
195}
196
197Session::Session(struct nc_session* session)
198 : m_session(session)
199{
200 impl::ClientInit::instance();
201}
202
203Session::~Session()
204{
205 ::nc_session_free(m_session, nullptr);
206}
207
208std::unique_ptr<Session> Session::connectPubkey(const std::string& host, const uint16_t port, const std::string& user, const std::string& pubPath, const std::string& privPath)
209{
210 impl::ClientInit::instance();
211
212 {
213 // FIXME: this is still horribly not enough. libnetconf *must* provide us with something better.
214 std::lock_guard lk(impl::clientOptions);
215 nc_client_ssh_set_username(user.c_str());
216 nc_client_ssh_set_auth_pref(NC_SSH_AUTH_PUBLICKEY, 5);
217 nc_client_ssh_add_keypair(pubPath.c_str(), privPath.c_str());
218 }
219 auto session = std::make_unique<Session>(nc_connect_ssh(host.c_str(), port, nullptr));
220 if (!session->m_session) {
221 throw std::runtime_error{"nc_connect_ssh failed"};
222 }
223 return session;
224}
225
226std::unique_ptr<Session> Session::connectSocket(const std::string& path)
227{
228 impl::ClientInit::instance();
229
230 auto session = std::make_unique<Session>(nc_connect_unix(path.c_str(), nullptr));
231 if (!session->m_session) {
232 throw std::runtime_error{"nc_connect_unix failed"};
233 }
234 return session;
235}
236
237std::vector<std::string_view> Session::capabilities() const
238{
239 std::vector<std::string_view> res;
240 auto caps = nc_session_get_cpblts(m_session);
241 while (*caps) {
242 res.emplace_back(*caps);
243 ++caps;
244 }
245 return res;
246}
247
248std::shared_ptr<libyang::Data_Node> Session::get(const std::string& filter)
249{
250 auto rpc = impl::guarded(nc_rpc_get(filter.c_str(), NC_WD_ALL, NC_PARAMTYPE_CONST));
251 if (!rpc) {
252 throw std::runtime_error("Cannot create get RPC");
253 }
254 auto reply = impl::do_rpc_data(this, std::move(rpc));
255 // TODO: can we do without copying?
256 // If we just default-construct a new node (or use the create_new_Data_Node) and then set reply->data to nullptr,
257 // there are mem leaks and even libnetconf2 complains loudly.
258 return libyang::create_new_Data_Node(reply->data)->dup_withsiblings(1);
259}
260
261std::string Session::getSchema(const std::string_view identifier, const std::optional<std::string_view> version)
262{
263 auto rpc = impl::guarded(nc_rpc_getschema(identifier.data(), version ? version.value().data() : nullptr, nullptr, NC_PARAMTYPE_CONST));
264 if (!rpc) {
265 throw std::runtime_error("Cannot create get-schema RPC");
266 }
267 auto reply = impl::do_rpc_data(this, std::move(rpc));
268
269 auto node = libyang::create_new_Data_Node(reply->data)->dup_withsiblings(1);
270 auto set = node->find_path("data");
271 for (auto node : set->data()) {
272 if (node->schema()->nodetype() == LYS_ANYXML) {
273 libyang::Data_Node_Anydata anydata(node);
274 return anydata.value().str;
275 }
276 }
277 throw std::runtime_error("Got a reply to get-schema, but it didn't contain the required schema");
278}
279
280std::shared_ptr<libyang::Data_Node> Session::getConfig(const NC_DATASTORE datastore, const std::optional<const std::string> filter)
281{
282 auto rpc = impl::guarded(nc_rpc_getconfig(datastore, filter ? filter->c_str() : nullptr, NC_WD_ALL, NC_PARAMTYPE_CONST));
283 if (!rpc) {
284 throw std::runtime_error("Cannot create get-config RPC");
285 }
286 auto reply = impl::do_rpc_data(this, std::move(rpc));
287 // TODO: can we do without copying?
288 // If we just default-construct a new node (or use the create_new_Data_Node) and then set reply->data to nullptr,
289 // there are mem leaks and even libnetconf2 complains loudly.
290 auto dataNode = libyang::create_new_Data_Node(reply->data);
291 if (!dataNode)
292 return nullptr;
293 else
294 return dataNode->dup_withsiblings(1);
295}
296
297void Session::editConfig(const NC_DATASTORE datastore,
298 const NC_RPC_EDIT_DFLTOP defaultOperation,
299 const NC_RPC_EDIT_TESTOPT testOption,
300 const NC_RPC_EDIT_ERROPT errorOption,
301 const std::string& data)
302{
303 auto rpc = impl::guarded(nc_rpc_edit(datastore, defaultOperation, testOption, errorOption, data.c_str(), NC_PARAMTYPE_CONST));
304 if (!rpc) {
305 throw std::runtime_error("Cannot create edit-config RPC");
306 }
307 impl::do_rpc_ok(this, std::move(rpc));
308}
309
310void Session::copyConfigFromString(const NC_DATASTORE target, const std::string& data)
311{
312 auto rpc = impl::guarded(nc_rpc_copy(target, nullptr, target /* yeah, cannot be 0... */, data.c_str(), NC_WD_UNKNOWN, NC_PARAMTYPE_CONST));
313 if (!rpc) {
314 throw std::runtime_error("Cannot create copy-config RPC");
315 }
316 impl::do_rpc_ok(this, std::move(rpc));
317}
318
319void Session::commit()
320{
321 auto rpc = impl::guarded(nc_rpc_commit(1, /* "Optional confirm timeout" how do you optional an uint32_t? */ 0, nullptr, nullptr, NC_PARAMTYPE_CONST));
322 if (!rpc) {
323 throw std::runtime_error("Cannot create commit RPC");
324 }
325 impl::do_rpc_ok(this, std::move(rpc));
326}
327
328void Session::discard()
329{
330 auto rpc = impl::guarded(nc_rpc_discard());
331 if (!rpc) {
332 throw std::runtime_error("Cannot create discard RPC");
333 }
334 impl::do_rpc_ok(this, std::move(rpc));
335}
336
337ReportedError::ReportedError(const std::string& what)
338 : std::runtime_error(what)
339{
340}
341
342ReportedError::~ReportedError() = default;
343}
344}